This tutorial guides you to learning XUL (XML User-interface Language) which is a cross-platform language for describing user interfaces of applications.
This tutorial will demonstrate creating a simple find file user interface, much like that provided by the Macintosh's Sherlock or the find file dialog in Windows. Note that only the user interface will be created and some limited functionality. The actual finding of files will be not be implemented. A blue line will appear to the left of a paragraph where the find file dialog is being modified. You can follow along at these sections.
XUL (pronounced zool and it rhymes with cool) was created to make development of the Mozilla browser easier and faster. It is an XML language so all features available to XML are also available to XUL.
Most applications need to be developed using features of a specific platform making building cross-platform software time-consuming and costly. This may not be important for some, but users may want to use an application on other devices such as handheld devices or set-top boxes.
A number of cross-platform solutions have been developed in the past. Java, for example, has portability as a main selling point. XUL is one such language designed specifically for building portable user interfaces.
It takes a long time to build an application even for only one platform. The time required to compile and debug can be lengthy. With XUL, an interface can be implemented and modified quicky and easily.
XUL has all the advantages of other XML languages. For example XHTML or other XML languages such as MathML or SVG can be inserted within it. Also, text displayed with XUL is easily localizable, which means that it can be translated into other languages with little effort. Style sheets can be applied to modify the appearance of the user interface (much like the skins or themes feature in WinAmp or some window managers).
XUL provides the ability to create most elements found in modern graphical interfaces. It is generic enough that it could be applied to the special needs of certain devices and powerful enough that developers can create sophisticated interfaces with it.
Some elements that can be created are:
The displayed content can be created from the contents of a XUL file or with data from a datasource. In Mozilla, such datasources include a user's mailbox, their bookmarks and search results. The contents of menus, trees and other elements can be populated with this data, or with your own data supplied in an RDF file.
XUL content may be loaded from a local file or a remote site. It may also be packaged into an installer which the user may download and install. This last method gives the application additional privileges, such as reading local files and modifying user preferences.
When loading XUL content from a remote site, you must set up your Web server to send XUL files with the content type 'application/vnd.mozilla.xul+xml'.
You should have an understanding of HTML and at least a basic understanding of XML and CSS. Here are some guidelines to keep in mind:
XUL is supported in Mozilla and browsers based upon it, such as Netscape 6. Due to various changes in XUL syntax over time, you will want to get the latest version for the examples to work properly.
We'll begin by looking at how the XUL file system is organized in Mozilla.
Mozilla is organized in such a way that you can have as many components as you want installed. A typical installation might have the navigator, messenger and editor components. It will also have one component for each installed skin and locale. Each of these components, or packages, is made up of a set of files that describe the user interface for it. For example, the messenger component will have descriptions of the mail messages list window, the composition window and the address book dialogs.
The packages that are provided with Mozilla are located within the chrome directory, which you will find in the directory where you installed Mozilla. The chrome directory is where all the files that describe the user interface used by Mozilla are located.
Each package may have a number of sub-packages. For example, the messenger package contains two sub-packages, the address book package, and the message composition package.
A package can supply its own skin information as part of the package, or it may rely on the default skin.
A user interface is made up of three sets of files, each stored in a separate subdirectory of the chrome directory. These three sets are the content, the skin and the locale. A particular component might provide one or more default skins and locales, but a user can replace them with their own.
Take a look at Mozilla's chrome directory. You should see a bunch of JAR files, one for each installed package. For example, the file messenger.jar describes the user interface for the Messenger component. The file modern.jar describes the Modern skin.
The name of the JAR file might describe what it contains, but you can be sure by viewing its contents. (You can view JAR files with a ZIP utility). Let's use the messenger package as an example. If you extract the files in messenger.jar, you will find that it contains a directory structure like the following:
content
messenger
contents.rdf
messenger.xul
-- other mail XUL and JS files goes here --
addressbook
-- address book files go here --
messengercompose
-- message composition files go here --
.
.
.
This is easily recognizable as a content package, as the top-level directory is called 'content'. For skins, this directory will be called 'skin' and for locales, it will be called 'locale'. Actually, this isn't necessary but you should follow this convention to make your package clearer. Some packages may include a content section, a skin and a locale. In this case, you will find a subdirectory for each type.
The content/messenger directory contains a number of files with xul and js extensions. The XUL files are the ones with the xul extension. The files with js extensions are JavaScript files which contain scripts that handle the functionality of a window. Many XUL files have a script file associated with them.
In the listing above, two files have been shown. There are others of course, but for simplicity they aren't shown. The file messenger.xul is the XUL file that describes the main Messenger window which displays the mail messages list. The main window for a content package should have the same name as the package with a xul extension. Some of the other XUL files are included by messenger.xul or describe separate windows. For example, the file subscribe.xul describes the dialog for subscribing to newsgroups.
The file contents.rdf is found in every package, one for each content, skin and locale in the package. This file is used to describe the contents of the package and will be described in detail later.
Two subdirectories, addressbook and messengercompose, describe additional sections of the Messenger component. They are placed in different directories only to separate them.
The modern.jar and classic.jar files describe the skins provided with Mozilla. Their structure is similar to the content packages. For example, modern.jar:
skin
modern
navigator
contents.rdf
-- navigator skin files go here --
messenger
contents.rdf
-- messenger skin files go here --
editor
contents.rdf
-- editor skin files go here --
communicator
contents.rdf
-- communicator skin files go here --
global
contents.rdf
-- global skin files go here --
.
.
.
Here, five directories exist, one for each package that the skin is applied to. For example, the editor directory describes the skin for the editor component. The global directory contains skin files that are general to all packages. These files will apply to all components. You will usually use this one yourself.
You might notice that there are five contents.rdf files. This way, the skins are applied separately to each component. You could have a different skin for navigator as messenger, but most of the appearance is determined by the global part so you won't see much difference. They are also separate files so that new components can be added and existing components can be removed easily.
A skin is made up of CSS files and a number of images used to define the look of an interface. The images are used for the toolbar buttons. The file messenger.css is used by messenger.xul and contains styles which define the appearance of various parts of the mail interface.
By changing the CSS file, you can adjust the appearance of a window without changing its function. This is how you can create a skin.
The file en-US.jar describes the language information for each component, in this case for US English. Like the skins, each language will contain files that specify text used by the package but for that language.
As usual, a contents.rdf file is provided that lists the packages the locale provides text for. Sundirectories provide the text for each package.
The localized text is stored in two types of files, DTD files, and properties files. The DTD files have a dtd extension and contain entity declarations, one for each text string that is used in a window. For example, the file messenger.dtd contains entity declarations for each menu command. In addition, keyboard shortcuts for each command are also defined, because they may be different for each language. DTD files are used by XUL files so, in general, you will have one per XUL file.
Properties files are similar, but are used by script files. The file messenger.properties contains a few such strings.
This structure allows you to translate Mozilla or a component into a different language by just adding a new locale for that language.
Many of the packages in Mozilla are sub-packages of the communicator package. For example, you'll find the bookmarks window, the history viewer and the preferences dialogs within the communicator package. They are put there because they are general to a number of packages. There is nothing special about them.
There is a special package called toolkit (or global). We saw the global directory earlier for skins. The toolkit.jar contains the corresponding content for it. It contains some global dialogs and definitions. It also defines the default appearance and functionality of the various widgets such as textboxes and buttons. The files located in the global part of a skin archive contains the default look for all of the XUL interface elements. Most skin changes will involve variations of these files.
The structure described above is where the default packages, skins and locales are stored. However, they do not need to be placed there. If you have another package installed, it can be placed anywhere on the disk. The file chrome.rdf in the chrome directory stores the list of installed packages, skins and locales and where they are located.
A user may have multiple skins and locales installed that modify the same package. Only one skin and locale per package is active at any given time. The file chrome/chrome.rdf in the user profile specifies which skins and locales are active.
The following section will describe how to refer to XUL documents and other chrome files.
XUL files can be referenced with a regular HTTP URL just like HTML files (Or any type of URL). However, packages that are installed into Mozilla's chrome system can be referenced with special chrome URLs. Files in the chrome directory will already be installed but you can register your own.
Installed packages have the advantage that they don't have security restrictions placed on them, which is necessary for many applications. Another advantage over other URL types is that they automatically handle mulitple skins and locales.
The basic syntax of a chrome URL is as follows:
chrome://<component>/content/<file.xul>
where <component> is the component name, such as messenger or editor.
Example: chrome://messenger/content/messenger.xul
The example here refers to the messenger window. You could point to a file that is part of a skin by replacing 'content' with 'skin' and changing the filename. Similarly, you can point to a file that is part of a locale by using 'locale' instead of 'content'.
You could think of the chrome URL as a special type of file URL (that is, file:// URLs) where the root directory is the Mozilla chrome directory. However, there is one additional feature. The skin and locale part of the chrome URL maps to the current setting. If the user changes the default skin, the chrome URL does not change, but what it points to changes.
Mozilla is able to figure out which skin current language are currently used and map the appropriate directories into chrome URLs. The file chrome.rdf in the chrome directory and the contents.rdf files are there to tell Mozilla how to do this. That way the user can use any skin or language but the URLs that reference chrome files do not have to be changed. For example, the default navigator.css can be referenced with:
chrome://navigator/skin/navigator.css
If you change the browser skin, the chrome URL does not change, even through the real location of the files used by the skin change.
The chrome system takes the navigator sections of the content, the current skin and the current locale and groups them together to form a user interface. Here are some more examples, this time for messenger:
chrome://messenger/content/messenger.xul
chrome://messenger/content/mime.js
chrome://messenger/skin/icons/images/folder-inbox.jpg
chrome://messenger/locale/messenger.dtd
For subpackages, the same structure can be used. The following URL will refer to the bookmarks window:
chrome://communicator/content/bookmarks/bookmarks.xul
You can enter chrome URLs anywhere normal URLs can be used. You can even enter them directly into the URL bar in a Mozilla browser window. Remember that you can also reference XUL files with regular file URLs also. You might do this to test a file without having to install a package.
You might also see chrome URLs without specified filenames, such as:
chrome://navigator/content/
This type of reference will automatically select an appropriate file from that directory. For content, a file with the name of the package and a xul extension is selected. In the above example, the file navigator.xul is selected.
For a skin, the file package.css is selected; for a locale, the file package.dtd is selected (where package is the name of the package). This is a convenient shorthand syntax.
In this section, we'll see how to put chrome and XUL files into a package and create manifest files for them.
A package is a set of XUL files and scripts that define the functionality of a user interface. Packages may be installed into Mozilla and referenced to with chrome URLs.
A package can contain any files it wants and may be split into subdirectories for different parts of the package. For example, the bookmarks and history viewer are part of the communicator package, but are stored in different sub-directories.
A package can be stored either as a directory or as a JAR archive. Each package will have a manifest file, contents.rdf, that describes the package. This file will be placed inside the JAR file alongside the files that it describes. The file must be named contents.rdf and be a file in RDF (Resource Description Framework) format. We'll learn more about RDF later.
The contents.rdf file describes the contents of a package. It can also be used to describe a skin or locale.
Manifest files are fairly easy to create once you know how. The template below can be used as a starting point.
<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:chrome="http://www.mozilla.org/rdf/chrome#">
<RDF:Seq about="urn:mozilla:package:root">
<RDF:li resource="urn:mozilla:package:myapplication"/>
</RDF:Seq>
<RDF:Description about="urn:mozilla:package:myapplication"
chrome:displayName="My Application"
chrome:author="name"
chrome:name="myapplication">
</RDF:Description>
</RDF:RDF>You can use this template and make some minor changes specific to your package. Let's break this down to understand what each piece does.
<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:chrome="http://www.mozilla.org/rdf/chrome#">
These three lines should be placed at the top of the contents.rdf file. Because RDF is an XML format, it contains the standard first line that XML files do. Next, we declare the namespaces that are being used, here one for RDF and the other for the chrome system. If you don't understand what this means, don't worry. Just add them at the top of the manifest file.
<RDF:Seq about="urn:mozilla:package:root"> <RDF:li resource="urn:mozilla:package:myapplication"/> </RDF:Seq>
These lines are used to declare what packages, skins and locales are described by the manifest file. In this case, a package is being described (as indicated by the word 'package' in the text). The name of the package here is 'myapplication'. Of course, you would replace this with the name of the package you were creating. For example, the Mozilla mail application has a name of 'messenger'.
The RDF:li tag above is much like the li tag of HTML as it declares an element of a list. Thus, you can declare multiple packages by using multiple RDF:li tags.
For skins, replace the two occurances of 'package' with 'skin'; for locales, replace the two occurances of 'package' with 'locale'. For example, the following specifies a skin:
<RDF:Seq about="urn:mozilla:skin:root"> <RDF:li resource="urn:mozilla:skin:blueswayedshoes"/> </RDF:Seq>
The only differences are the changes of the two occurances of the word 'package' to 'skin' and the change to the skin name 'blueswayedshoes'.
<RDF:Description about="urn:mozilla:package:myapplication"
chrome:displayName="My Application"
chrome:author="name"
chrome:name="myapplication">
</RDF:Description>
This block is used to provide more details of the package, skin or locale. You will need a description for each li that you have. The value of the about attribute should be equal to the resource attribute on the li tag.
The three extra attributes describe extra information about the package:
Let's create a contents.rdf file for the find files dialog we'll be creating. It will need to describe the package. Because there are no sub-packages, skins or locales included, it is fairly similar to the template above.
<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:chrome="http://www.mozilla.org/rdf/chrome#">
<RDF:Seq about="urn:mozilla:package:root">
<RDF:li resource="urn:mozilla:package:findfile"/>
</RDF:Seq>
<RDF:Description about="urn:mozilla:package:findfile"
chrome:displayName="Find Files"
chrome:author="Whoever"
chrome:name="findfile">
</RDF:Description>
</RDF:RDF>Here, the component is named 'findfile' which means that we'll be able to refer to it using the following chrome URL:
chrome://findfile/content/findfile.xul
The list of installed packages is stored in the chrome directory in the file chrome.rdf. It will be automatically changed when you install a new package. Like the manifest files, it is an RDF format file. On first inspection, it looks quite different than the manifest files, but if you are familiar with RDF, you should notice that it is actually very similar.
Conveniently, we don't need to edit this file directly. Whenever Mozilla starts up, it checks the chrome directory for a special file called 'installed-chrome.txt'. This file contains a list, in a very simple format, of all the packages, skins and locales that are waiting to be installed. Mozilla scans each entry in the list and installs or updates each one as necessary.
So, to install a new package, all you need to do is add an entry to the file 'installed-chrome.txt' and restart Mozilla. Mozilla also has an installation system called XPInstall which will allow scripts to install packages automatically via JavaScript without having to modify this file manually. XPInstall will be described near the end of the tutorial. However, during development, we can modify the file directly.
The file 'installed-chrome.txt' should be added directly into the chrome directory, if it is not already there. The file contains a list of entries to install, one per line. For example:
content,install,url,resource:/chrome/findfile/content/findfile/ skin,install,url,resource:/chrome/findfile/skin/findfile/
The above will be used to install the findfiles package and also a skin for it. The format of each line is fairly simple. It's just four values separated by commas:
The resource URL is similar to a file URL except that its root is the directory where Mozilla is installed. The resource URL should have only one slash after the colon. We will see more of the resource URL in the next section.
Thus, the line added should point to the directory where the contents.rdf file is located. If you have multiple packages, add a line for each one.
Although Mozilla follows a directory naming convention, you can put the files anywhere you want. For example, the following will install a new package that is located in the directory /main/calculator/.
content,install,path,/main/calculator/
If you are packing your files into a JAR file, you can use a JAR URL to refer to it. It has two parts, separated by an exclamation mark (!). The part before is the URL of the JAR file and the part after is the directory or file within the archive. The example below might refer to the find files dialog:
jar:resource:/chrome/findfile.jar!/content/findfile/
In summary, the following steps are needed to install a package, skin or locale. You do not need to install XUL files, but you will be limited in what scripts will be allowed to do.
We're going to be creating a simple find files utility throughout this tutorial. First, however, we should look at the basic syntax of a XUL file.
A XUL file can be given any name but it really should have a .xul extension. The simplest XUL file has the following structure:
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window
id="findfile-window"
title="Find Files"
orient="horizontal"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
...
</window>A breakdown line by line is necessary:
In order to open a XUL window, there are several methods that can be used. If you are only in the development stage, you can just type the URL (whether a chrome:, file: or other URL type) into the location bar in a Mozilla browser window. The XUL window will appear in the browser window as opposed to a new window, but this is often sufficient during the early stages of development.
The correct way, of course, is to open the window using JavaScript. No new syntax is necessary as you can use the window.open() function as one can do for HTML documents. However, one additional flag, called 'chrome' is necessary to indicate to the browser that this is a chrome document to open. This will open without the toolbars and menus and so forth that a normal browser window has. The syntax is described below:
window.open(url,windowname,flags);
where the flags contains the flag "chrome".
Example:
window.open("chrome://navigator/content/navigator.xul", "bmarks",
"chrome,width=600,height=300");
Let's begin by creating the basic file for the find file dialog. Create a file called findfile.xul and put it in a new directory somewhere. It doesn't matter where you put it, but the chrome/findfile/findfile/content directory is a suitable place. Add the XUL template shown at the top of this page to the file and save it.
Instead of a file URL, we could use a resource URL. This type of URL is similar to a file URL, except that it begins with 'resource:' instead of 'file:' and its top-level directory is the directory where Mozilla is installed instead of the root of the file system. This means that it can be used to refer to files in the Mozilla directory or its subdirectories, regardless of where Mozilla has been installed. Thus, we could refer to the find files dialog with the following resource URL:
resource:/chrome/findfile/content/findfile/findfile.xul
You can use the command-line parameter '-chrome' to specify the XUL file to open when Mozilla starts. If this is not specified, the default window open will open. (Usually the browser window.) For example, we could open the find files dialog with either of the following:
mozilla -chrome chrome://findfile/content/findfile.xul
mozilla -chrome resource:/chrome/findfile/content/findfile/findfile.xul
If you run this command from a command-line (assuming you have one on your platform), the find files dialog will open by default instead of the Mozilla browser window. Of course, because we haven't put anything in the window, nothing will appear to have happened.
To see the effect though, the following will open the bookmarks window:
mozilla -chrome chrome://communicator/content/bookmarks/bookmarks.xul
In this section, we will look at how to add some simple buttons to a window.
The window we've created so far has had nothing in it, so it isn't very interesting yet. In this section, we will add two buttons, a Find button and a Cancel button. We will also learn a simple way to position them on the window.
Like HTML, XUL has a number of tags that can be used to create user interface elements. The most basic of these is the button tag. This element is used to create simple buttons.
The button element has two main properties associated with it, a label and an image. You need one or the other or both. Thus, a button can have a label only, an image only or both a label and an image. Buttons are commonly used for the buttons on toolbars and for the OK and Cancel buttons in a dialog.
The button tag has the following syntax:
<button
id="identifier"
class="dialog"
label="OK"
image="images/image.jpg"
default="true"
disabled="true"
accesskey="t"/>The attributes are as follows, all of which are optional:
Note that a button supports more attributes than those listed above. Others will be discussed later.
By default, buttons will appear on the window without any special appearance. Only the text and image will appear with a raised border around it. In some cases, such as an OK button, this may be what you want. In other cases however, you may want to have a different border appear around the button. Or, you may want the text to highlight when the user moves the mouse over the button. The usual way to do this is much like in HTML by changing the styles of a button using style sheets or scripts.
Some examples of buttons:
Example 2.2.1: Source View<button label="Normal"/> <button label="Disabled" disabled="true"/> <button label="Default" default="true"/>
The examples above will generate the buttons in the image. The
first button is a normal button. The second button is disabled so it appears
greyed out. The third button is the default button and has a thicker border.
The image here is for the modern skin. Other skins will render disabled
and default buttons differently.
We'll start by creating a simple Find button for the find files utility. The example code below shows how to do this:
<button id="find-button" label="Find" default="true"/>
This code creates a simple button with the label Find. This button is specified to be the default button, meaning that the user can press the ENTER key to activate the Find button.
Let's add this code to the file findfile.xul that we created in the previous section. The code needs to be inserted in-between the window tags. The code to add is shown in red below:
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window
id="findfile-window"
title="Find Files"
orient="horizontal"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<button id="find-button" label="Find" default="true"/>
<button id="cancel-button" label="Cancel"/>
</window>You'll notice that the Cancel button was added also. It should not be the default button so this attribute was left off. The window has been given a horizontal orientation so that the two buttons appear beside each other.
If you open the file in Mozilla, you should get something like the image
shown here. Notice that the Find button has appeared with a thicker border.
The effect is caused by the default attribute on the
find button. Later, we'll show how you could override this addition, although
it's best to leave it as is as this may be the desired effect on some platforms.
This section describes a way to add labels and images to a window. In addition, we look at how to include elements into groups.
You cannot embed text directly into a XUL file without tags around it and expect it to be displayed. The most basic way to include text in a window is to use the description element. An example is shown below:
Example 2.3.1: Source View<description value="This is some text"/>
The value attribute can be used to specify the text that you wish to have displayed. The text will not wrap, so the text will appear on only one line. This is suitable for short sections of text.
For longer text, you can place content inside the opening and closing description tags. Unlike text specified with the value attribute, the child text will wrap onto multiple lines if necessary. Like HTML, line breaks and extra whitespace are collapsed into a single space. Later we'll find out how to constrain the width of elements so that we can see the wrapping.
Example 2.3.2: Source View<description> This longer section of text is displayed. </description>
A second element, label, can be used in a similar way, except it is intended for labels of controls such as buttons and textboxes. You can use the control attribute to set which control the label is associated with. When the user clicks the label, that control will be focused. Set the value of the control attribute to the id of the element to be focused.
Example 2.3.3: Source View<label value="Click here:" control="open-button"/> <button id="open-button" label="Open"/>
In the example above, clicking the label will cause the button to be focused.
Like HTML, XUL has an element to display images within a window. This element is appropriately named image. Note that the tag name is different than HTML (image instead of img). You can use the src attribute to specify the URL of the image file. The example below shows this:
<image src="images/banner.jpg"/>
Although you can use this syntax, it would be better to use a style sheet to set the image URL. You can use the list-style-image CSS property to set the URL for the image. Here are some examples:
XUL:
<image id="image1"/>
<image id="search"/>
Style Sheet:
#image1 {
list-style-image="chrome://findfile/skin/banner.jpg";
}
#search {
list-style-image="chrome://findfile/skin/images/search.jpg";
}These images come from within the chrome directory, in the skin for the findfile package. Because images vary by skin, you would usually place images in the skin directory.
XUL has elements that are similar to the HTML form controls.
HTML has an input element which can used for text entry controls (along with other form controls). XUL has a similar element used for straight text entry fields. The element is the textbox element. Without any attributes, the textbox element creates a box in which the user can enter text. Textboxes accept many of the same attributes as HTML input controls. The following are some of them:
The following example shows some textboxes:
Example 2.4.1: Source View<label control="some-text" value="Enter some text"/> <textbox id="some-text"/> <label control="some-password" value="Enter a password"/> <textbox id="some-password" type="password" maxlength="8"/>
The textbox examples above will create text inputs that can only be used for entering one line of text. HTML also has a textarea element for creating a larger text entry area. In XUL, you can use the textbox element for this purpose as well -- two separate elements are not necessary. If you set the multiline attribute to true, the text entry field will display mutliple rows.
For example:
Example 2.4.2: Source View<textbox multiline="true"
value="This is some text that could wrap onto multiple lines."/>Like the HTML textarea, you can use the rows and cols attributes to set the size. This should be set to the number of rows and columns of characters to display.
Let's add a search entry field to the find file dialog. We'll use the textbox element.
<label value="Search for:" control="find-text"/>
<textbox id="find-text"/>
<button id="find-button" label="Find" default="true"/>Add these lines before the buttons we created in the last section. If you open this window, you will see something much like that shown in the image below.
Notice that the label and the text input field have now appeared in the
window. The textbox is fully functional and you can type into it and
select text. Note that the control attribute
has been used so that the textbox is selected when the label is clicked.
Two additional elements are used for creating check boxes and radio buttons. They are variations of buttons. The checkbox is used for options that can be enabled or disabled. Radio buttons can be used for a similar purpose when there are a set of them where only one can be selected at once.
You can use most of the same attributes on checkboxes and radio buttons as with buttons. The example below shows some simple checkboxes and radio buttons.
<checkbox id="case-sensitive" checked="true" label="Case sensitive"/> <radio id="orange" checked="false" label="Orange"/> <radio id="violet" checked="true" label="Violet"/> <radio id="yellow" checked="false" label="Yellow"/>
The first line creates a simple checkbox. When the user clicks the checkbox, it switches between checked and unchecked. The checked attribute can be used to indicate the default state. You should set this to either true or false. The label attribute can be used to assign a label that will appear beside the check box. The radio button is similar.
In order to group radio buttons together, you need to use the radiogroup element. Only one of the radio buttons in a radio group can be selected at once. Clicking one with turn off all of the others in the same group. The following example demonstrates this.
Example 2.4.3: Source View<radiogroup> <radio id="orange" checked="false" label="Orange"/> <radio id="violet" checked="true" label="Violet"/> <radio id="yellow" checked="false" label="Yellow"/> </radiogroup>
Check boxes and radio buttons are simply specialized buttons. Remember that buttons are made up of a label and an image. A check box is just a label with an image of a check mark. You can also use many of the same attributes as buttons:
XUL has a number of types of elements for creating list boxes.
A list box is used to display a number of items in a list. The user may select an item from the list.
XUL provides two types of elements to create lists, a listbox element to create multi-row list boxes, and a menulist element to create drop-down list boxes. They work similar to the HTML select element, which performs both functions, but the XUL elements have additional features.
The simplest list box uses the listbox element for the box itself, and the listitem element for each item. For example, this list box will have four rows, one for each item.
Example 2.5.1: Source View<listbox> <listitem label="Butter Pecan"/> <listitem label="Chocolate Chip"/> <listitem label="Raspberry Ripple"/> <listitem label="Squash Swirl"/> </listbox>
Like with the HTML option element, you can assign a
value for each item using the value attribute. You
can then use the value in a script. The list box will default to a suitable size,
but you can control the size with the rows attribute.
Set it to the number of rows to display in the list box. A scroll bar will appear
that the user can use to display the additional rows.
The following example demonstrates these additional features:
Example 2.5.2: Source View<listbox rows="3"> <listitem label="Butter Pecan" value="bpecan"/> <listitem label="Chocolate Chip" value="chocchip"/> <listitem label="Raspberry Ripple" value="raspripple"/> <listitem label="Squash Swirl" value="squash"/> </listbox>
The example has been changed to display only 3 rows at a time. Values have also been added to each item in the list. List boxes have many additional features, which will be described later.
Drop-down lists can be created in HTML using the select element. The user can see a single choice in a textbox and may click the arrow or some similar such button next to the textbox to make a different selection. The other choices will appear in a pop-up window. XUL has a menulist element which can be used for this purpose. It is made from a textbox and a button beside it. It's name was chosen because it pops up a menu with the choices in it.
Three elements are needed to describe a drop-down box. The first is the menulist element, which creates the textbox with the button beside it. (For non-editable menulists, the textbox will be a button). The second, menupopup, creates the popup window which appears when the button is clicked. The third, menuitem, creates the individual choices.
It's syntax is best described with the example below:
Example 2.5.3: Source View<menulist label="Bus">
<menupopup>
<menuitem label="Car"/>
<menuitem label="Taxi"/>
<menuitem label="Bus" selected="true"/>
<menuitem label="Train"/>
</menupopup>
</menulist>
This menulist will contain four choices, one for each
menuitem element. To show the choices, click the
arrow button on the menulist. When one is selected, it appears as the choice
in the menulist. The selected attribute is used to
indicate the value that is selected by default.
By default, you can only select choices from the list. You cannot enter your own selection by typing it, like you can in some drop-down boxes. A variant of the menulist allows this. Instead of a button for displaying the current selection, a textbox is used so that you can type the selection in. For example, the URL field in Navigator has a drop-down for selecting previously typed URLs, but you can also type them in yourself.
To create an editable menulist, add the editable attribute as follows:
Example 2.5.4: Source View<menulist editable="true">
<menupopup>
<menuitem label="www.mozilla.org"/>
<menuitem label="www.xulplanet.com"/>
<menuitem label="www.dmoz.org"/>
</menupopup>
</menulist>The URL field created here has three pre-populated choices that the user can select or they can enter one of their own by typing it into the field. The text the user enters is not added as a new choice. Because the value attribute was not used in this example, the default value will be blank.
Now that we've added some buttons, let's add some other elements.
In addition to all of the XUL elements that are available, you can also add HTML elements directly within a XUL file. You can actually use any HTML element in a XUL file, meaning that Java applets and tables can be placed in a window. You should avoid using HTML elements in XUL files if you can. However, this section will describe how to use them anyways. Remember that XML is case-sensitive though, so you'll have to enter the tags and attributes in lowercase.
In order to use HTML elements in a XUL file, you must declare that you are doing so using the XHTML namespace. This way, Mozilla can distinguish the HTML tags from the XUL ones. The attribute below should to be added to the window tag of the XUL file, or to the outermost HTML element.
xmlns:html="http://www.w3.org/1999/xhtml"
This is a declaration of HTML much like the one we used to declare XUL. This must be entered exactly as shown or it won't work correctly. Note that Mozilla does not actually download this URL, but it does recognize it as being HTML.
Here is an example as it might be added to the find file window:
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window
id="findfile-window"
title="Find Files"
orient="horizontal"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">Then, you can use HTML tags as you would normally, keeping in mind the following:
You can use any HTML tag although some such as head and body are not really useful. Some examples of using HTML elements are shown below.
<html:img src="banner.jpg"/>
<html:input type="checkbox" value="true"/>
<html:table>
<html:tr>
<html:td>
A simple table
</html:td>
</html:tr>
</html:table>These examples will create an image from the file banner.jpg, a checkbox and a single-cell table. You should always use XUL features if they are available and you probably should not use tables for layout in XUL. (There are XUL elements for doing layout). Notice that the prefix html: was added to the front of each tag. This is so that Mozilla knows that this is an HTML tag and not a XUL one. If you left out the html: part, the browser would think that the elements were XUL elements and they would not display because img, input, table, and so on are not valid XUL tags.
In XUL, you can add labels with the description or label element. You should use these elements when you can. You can also add labels to controls either by using the HTML label element, or you can simply put the text inside another HTML block element (such as p or div) as in the example below.
Example 2.6.1: Source View<html:p> Search for: <html:input id="find-text"/> <button id="okbutton" label="OK"/> </html:p>
This code will cause the text 'Search for:' to be displayed, followed by an input element and an OK button. Notice that the XUL button can appear inside the HTML elements, as it does here. Plain text will only be displayed when placed inside an HTML element that would normally allow you to display text (such as a p tag). Text outside of one will not be displayed, unless the XUL element the text is inside allows this (the description element, for example). The examples below may help.
What follows are some examples of adding HTML elements to windows. In each case, the window and other common information has been left out for simplicitiy.
1. A dialog with a check box
Example 2.6.2: Source View<html:p>
Click the box below to remember this decision.
<html:p>
<html:input id="rtd" type="checkbox"/>
<html:label for="rtd">Remember This Decision</html:label>
</html:p>
</html:p>In this case, one p tag was used to place the text in and another was used to break apart the text into multiple lines.
2. Text outside of HTML blocks
Example 2.6.3: Source View<html:div>
Would you like to save the following documents?
<html:hr/>
</html:div>
Expense Report 1
What I Did Last Summer
<button id="yes" label="Yes"/>
<button id="no" label="No"/>
As can be seen in the image, the text inside the div tag was displayed but the other text (Expense Report 1 and What I Did Last Summer) was not. This is because there is no HTML or XUL element capable of displaying text enclosing it. To have this text appear, you would need to put it inside the div tag, or enclose the text in a description tag.
3. Invalid HTML elements
<html:po>Case 1</html:po> <div>Case 2</div> <html:description value="Case 3"/>
All three of the cases above will not display, each for a different
reason.
Case 1: po is not a valid
HTML tag and Mozilla has no idea what to do with it.
Case 2: div is valid but only
in HTML. To get it to work, you will need to add the html: qualifier.
Case 3: A description element is only
valid in XUL and not in HTML. It should not have the html: qualifier.
In this section, we'll find out how to add some spacing in between the elements we have created.
One of the problems with developing user interfaces is that each user has a different display. Some users may have larger displays with higher resolutions and others may have lower resolutions. In addition, different platforms may have special requirements on the user interface. If adding support for multiple languages, the text for one language may require more room than another.
Applications that need to support multiple platforms and languages usually have their windows laid out with lots of space to allow for this. Some platforms and user interface toolkits provide components that are smart enough to resize and re-position themselves to fit the needs of the user. (Java uses layout managers for example.)
XUL provides the capability for elements to position and resize automatically. As we've seen, the find files window has appeared in a size that will fit the elements inside it. Each time we add something, the window gets bigger.
XUL uses a layout system called the 'Box Model'. We'll talk more about this is the next section but it essentially allows you to divide a window into a series of boxes that hold elements. The boxes will be positioned and resize based on specifications that you can define. For now, just know that the window element is a type of box.
Before we get into detail about boxes, we'll introduce another XUL element that is useful for layout, the spacer. A spacer is very simple and only requires one attribute, which will be explained in a moment. The simplest spacer looks like the following:
<spacer flex="1"/>
A spacer is used to place blank space into a window. Its most useful ability is that is can grow or shrink as the user resizes the window. This would be how one would place buttons on the right or bottom of a window and have them stick to the right or bottom edge no matter what size the window is. As we'll see, you can use a series of spacers to create a number of layout effects.
In this syntax above, the spacer has one attribute, called flex. This is used to define the flexibility of the spacer. In the case above, the spacer has a flex of 1. This makes the spacer element stretchy. If you place a spacer directly inside a window, the spacer will grow in size when the size of the window is changed.
We'll add a spacer to our find file dialog soon. First, let's take a look at what happens when the current dialog is resized.
If you change the size of the find files window, you can see that the elements have remained where they started. None of them have been moved or resized even though the window has more room in it. Let's see what happens when a spacer is added between the text box and the Find button.
By adding a spacer and resizing the window, you can see that the spacer has expanded to fill the space. The buttons have been pushed over. The code to add a spacer is shown below. Insert it just before the Find button.
<spacer flex="1"/>
<button id="find-button" label="Find" default="true"/>XUL lays out elements on a window by calculating suitable widths and heights for the elements and then adding space where they are flexible. Unless you specify information about the width and height of an element, the default size of an element is determined by its contents. You'll notice that the Cancel button in the dialogs has always set its width so that it fits the text inside it. If you create a button with a very long label, the button's default size will be large enough to hold the entire label. Other elements, such as the text box have chosen a suitable default size.
The flex attribute is used to specify if an element can change size to fit the box (in this case, the window) it is in. We've already seen the flex attribute applied to spacers, but it can be applied to any element. For example, you might want to have the Find button resize instead.
As you can see in the image, by placing a flex attribute on the Find button, it resizes when the window does. A spacer is really nothing special. It could really be considered a hidden button. It works much the same way as a button except it does not draw on screen.
You may have noticed something about the image above. Not only did the Find button grow in size but some space has appeared between the main label and the button. Of course, this is the spacer that we put in earlier. It has resized itself also. If you look closely enough, you should notice that the change in size has been divided up equally between the spacer and the button. The spacer has received half of the free space and the button has received the other half.
The reason we're seeing this effect is that both the spacer and the Find button have a flex attribute. Because both are flexible, both the button and the spacer resize equally.
What if you want to set one element to grow twice as large an another? You can use a higher number as the value of the flex attribute. The values of the flex element are a ratio. If one element has a flex of 1 and the next one has a flex of 2, the second one will grow at twice the rate of the first one. In effect, a flex of 2 says that this element has a flex that is two times the elements that have a flex of one.
The flex attribute only hints as to what the size ratio between elements should be. For various reasons, including explicit sizes set on elements, or special requirements of certain widgets, the flexibility may not be honored exactly.
The flex attribute can be placed on any element incuding HTML elements. However it only has any meaning when placed on an element directly inside a box. We'll look at boxes in the next section. This means that even though you can place a flex on an HTML element, it will have no effect if that element is inside a non-box element.
Let's see some examples:
Example 1:
<button label="Find" flex="1"/>
<button label="Cancel" flex="1"/>
Example 2:
<button label="Find" flex="1"/>
<button label="Cancel" flex="10"/>
Example 3:
<button label="Find" flex="2"/>
<button label="Replace"/>
<button label="Cancel" flex="4"/>
Example 4:
<button label="Find" flex="2"/>
<button label="Replace" flex="2"/>
<button label="Cancel" flex="3"/>
Example 5:
<html:div>
<button label="Find" flex="2"/>
<button label="Replace" flex="2"/>
</html:div>
Example 6:
<button label="Find" flex="145"/>
<button label="Replace" flex="145"/>
Specifying a flex value of 0 has the same effect as leaving the flex attribute out entirely. It means that the element is not flexible at all.
You may have noticed that when you resize the find file dialog vertically, the buttons resize themselves to fit the height of the window. This is because all of the buttons have an implied vertical flex given to them by the window. In the next section we'll talk about changing this.
You may sometimes see a flex value specified as a percentage. This has no special meaning and is treated as if the percent sign was not there.
In this section, we will look at some additional features of buttons.
You can add an image to a button by specifying a URL in the image attribute. The image is loaded from the URL, which can be a relative or absolute URL, and then the image is displayed on the button.
The button below will have both a label and the image 'happy.png'. The image will appear to the left of the label. You can change this position by using two other attributes. This will be explained in a moment.
Example 2.8.1: Source View<button label="Help" image="happy.png"/>
Another way to specify the image by using the CSS list-style-image style property on the button. This is designed to allow the 'skin' (in this case, the appearance of the image) to be changed without changing the XUL file. An example is shown below.
Example 2.8.2: Source View<button id="find-button"
label="Find" default="true" style="list-style-image: url('happy.png')"/>In this case, the image 'find.png' is displayed on the button. The style attribute functions similar to its HTML counterpart. In general, it can be used on all XUL elements. Note that you really should put the style declarations in a separate style sheet.
By default, the image on a button will appear to the left of the text label. There are two attributes that can be used to control this position.
The dir attribute controls the direction of the image and text. By setting this attribute to the value rtl, which stands for right-to-left, the image will be placed on the right side of the text. By using the value ltr, or leaving the attribute out entirely, the image will be placed on the left side of the text.
The orient attribute can be used to place the image above or below the text. The default value is horizontal which is used to place the image on the left or right. You can also use the value vertical to place the image above or below. In this case, the dir attribute controls the placement above or below. The same values are used, where ltr means place the image above the text, and rtl means place the image below the text.
Example 2.8.3: Source View<button label="Left" image="happy.png"/> <button label="Right" image="happy.png" dir="rtl"/> <button label="Above" image="happy.png" orient="vertical"/> <button label="Below" image="happy.png" orient="vertical" dir="rtl"/>
The example here shows all four types of alignment of buttons. Note that the two attributes are not specified when the default value can be used.
Buttons may have arbitrary markup contained inside them, and it will be rendered inside the button. You probably wouldn't use this very often, but you might use it when creating custom elements.
For example, the following will create a button where two of the words are purple:
Example 2.8.4: Source View<button> <description value="This is a"/> <description value="rather strange" style="color: purple;"/> <description value="button"/> </button>
Any XUL element may be placed inside the button. HTML elements will be ignored, so you need to wrap them inside a description element. If you specify the label attribute on the button, it will override any content placed inside the button.
You can place a menupopup inside the button to cause a menu to drop down when the button is pressed, much like the menulist. However, in this case you must set the type attribute to the value menu.
Example 2.8.5: Source View<button type="menu" label="Device">
<menupopup>
<menuitem label="Printer"/>
<menuitem label="Mouse"/>
<menuitem label="Keyboard"/>
</menupopup>
</button>In this example, the user may click the button to pop up a menu containing three items. Note that selecting one of these menu items doesn't change the label on the button, unlike a menulist. This type of button is intended as menu, with scripts attached to each item to perform a task. We'll see more on menus this later.
You can also set the type attribute to the value
menu-button. This also creates a button with a menu,
but the appearance will be different. The image to the right shows the difference.
The left one is a 'menu' and the second one is a 'menu-button'. It has an arrow
indicating the presence of a menu. For the 'menu', the user may click anywhere on
the button to show the menu. For the 'menu-button', the user must click the arrow
to show the menu.
In this section, we'll look at how XUL handles layout.
The main form of layout in XUL is called the 'Box Model'. This model allows you to divide a window into a series of boxes. Elements inside of a box will orient themselves horizontally or vertically. By combining a series of boxes, spacers and elements with flex attributes, you can control the layout of a window.
Although a box is the fundamental part of XUL element layout, it follows a few very simple rules. A box can lay out its children in one of two orientations, either horizontally or vertically. A horizontal box lines up its elements horizontally and a vertical box orients its elements vertically. You can think of a box as one row or one column from an HTML table. Various attributes placed on the child elements in addition to some CSS style properties control the exact position and size of the children.
The basic syntax of a box is as follows:
<hbox> ... </hbox> <vbox> ... </vbox>
The hbox element is used to create a horizontally oriented box. Each element placed in the hbox will be placed horizontally in a row. The vbox element is used to create a vertically oriented box. Added elements will be placed underneath each other in a column.
There is also a generic box element which defaults to horizontal orientation, meaning that it is equivalent to the hbox. However, you can use the orient attribute to control the orientation of the box. You can set this attribute to the value horizontal to create a horizontal box and vertical to create a vertical box.
Thus, the two lines below are equivalent:
<vbox> <box orient="vertical">
The following example shows how to place three buttons vertically.
Example 3.1.1: Source View<vbox> <button id="yes" label="Yes"/> <button id="no" label="No"/> <button id="maybe" label="Maybe"/> </vbox>
The three buttons here are oriented vertcally as was indicated by the box.
To change the buttons so that they are oriented horizontally, all you need to
do is change the vbox element to a
hbox element.
You can add as many elements as you want inside a box, including other boxes. In the case of a horizontal box, each additional element will be placed to the right of the previous one. The elements will not wrap at all so the more elements you add, the wider the window will be. Similarly, each element added to a vertical box will be placed underneath the previous one. The example below shows a simple login prompt:
Example 3.1.2: Source View<vbox>
<hbox>
<label control="login" value="Login:"/>
<textbox id="login"/>
</hbox>
<hbox>
<label control="pass" value="Password:"/>
<textbox id="pass"/>
</hbox>
<button id="ok" label="OK"/>
<button id="cancel" label="Cancel"/>
</vbox>
Here four elements have been oriented vertically, two inner
hbox tags and two
button elements. Notice that only the
elements that are direct descendants of the box are oriented vertcally.
The labels and textboxes are inside the inner hbox
elements, so they are oriented according to those boxes, which are horizontal.
You can see in the image that each label and textbox is oriented horizontally.
If you look closely at the image of the login dialog, you can see that the two textboxes are not aligned with each other horizontally. It would probably be better if they were. In order to do this we need to add some additional boxes.
Example 3.1.3: Source View<vbox>
<hbox>
<vbox>
<label control="login" value="Login:"/>
<label control="pass" value="Password:"/>
</vbox>
<vbox>
<textbox id="login"/>
<textbox id="pass"/>
</vbox>
</hbox>
<button id="ok" label="OK"/>
<button id="cancel" label="Cancel"/>
</vbox>
Notice how the text boxes are now aligned with each other. To do this, we
needed to add boxes inside of the main box. The two labels and textboxes
are all placed inside a horizontal box. Then, the labels are placed inside
another box, this time a vertical one, as are the textboxes. This inner
box is what makes the elements orient vertically. The horizontal box is
needed as we want the labels vbox and the inputs vbox to be placed horizontally
with each other. If this box was removed, both textboxes would appear below
both of the labels.
The issue now is that the 'Password' label is too high. We should really use the grid element here to fix this which we'll learn about in a later section.
Let's add some boxes to the find files dialog. A vertical box will be added around all of the elements, and a horizontal box with be added around the textbox and the buttons. The result will be that the buttons will appear below the textbox.
<vbox flex="1"> <description> Enter your search criteria below and select the Find button to begin the search. </description> <hbox> <label value="Search for:" control="find-text"/> <textbox id="find-text"/> </hbox> <hbox> <spacer flex="1"/> <button id="find-button" label="Find" default="true"/> <button id="cancel-button" label="Cancel"/> </hbox> </vbox>
The vertical box causes the main text, the box with the textbox and the box with the buttons to orient vertically. The inner boxes orient their elements horizontally. As you see in the image below, the label and text input are placed side by side. The spacer and two buttons are also placed horizontally in their box. Notice how the spacer causes the buttons to appear on the right side, because it is flexible.
Here we'll look at controlling the position and size of an element.
So far, we know how to position elements either horizontally or vertically inside a box. We will often need more control over the position and size of elements within the box. For this, we first need to look at how a box works.
The position of an element is determined by the layout style of its container. For example, the position of a button in a horizontal box is to the right of the previous button, if any. The size of an element is determined by two factors, the size that the element wants to be and the size you specify. The size that an element wants to be is determined by what is in the element. For example, a button's width is determined by the amount of text inside the button.
An element will generally be as large as it needs to be to hold its contents, and no larger. Some elements, such as textboxes have a default size, which will be used. A box will be large enough to hold the elements inside the box. A horizontal box with three buttons in it will be as wide as the three buttons, plus a small amount of padding.
In the image below, the first two buttons have been given a suitable size to hold their text. The third button is larger because it contains more content. The width of the box containing the buttons is the total width of the buttons plus the padding between them. The height of the buttons is a suitable size to hold the text.
You may need to have more control over the size of an element in a window. There are a number of features that allow you to control the size of an element. The quick way is to simply add the width and height attributes on an element, much like you might do on an HTML img tag. An example is shown below:
Example 3.2.1: Source View<button label="OK" width="100" height="40"/>
However, it is not recommended that you do this. It is not very portable and may not fit in with some themes. A better way is to use style properties, which work similarly to style sheets in HTML. The following CSS properties can be used.
By setting either of the two properties, the element will be created with that width and height. If you specify only one size property, the other is calculated as needed. The size of these style properties should be specified as a number followed by a unit.
The sizes are fairly easy to calculate for non-flexible elements. They simply obey their specified widths and heights, and if the size wasn't specified, the element's default size is just large enough to fit the contents. For flexible elements, the calculation is slightly trickier.
Flexible elements are those that have a flex attribute set to a value greater than 0. Recall that flexible elements grow and shrink to fit the available space. Their default size is still calculated the same as for inflexible elements. The following example demonstrates this:
Example 3.2.2: Source View<box> <button label="Yes" flex="1"/> <button label="No"/> <button label="I really don't know one way or the other"/> </box>
The window will initially appear like in the image earlier. The first two buttons will be sized at a suitable default width and the third button will be larger because it has a longer label. The first button is made flexible and all three elements have been placed inside a box. The width of the box will be set to the initial total width of all three buttons (around 430 pixels in the image).
If you increase the width of the window, elements are checked to see whether they are flexible to fill the blank space that would appear. The button is the only flexible element, but it will not grow wider. This is because the box that the button is inside is not flexible. An inflexible element never changes size even when space is available, so the button can't grow either. Thus, the button won't get wider.
The solution is to make the box flexible also. Then, when you make the window wider, extra space will be available, so the box will grow to fill the extra space. Because the box is larger, more extra space will be created inside it, and the flexible button inside it will grow to fit the available space. This process repeats for as many nested boxes as necessary.
You may want to allow a element to be flexible but constrain the size so that it cannot be larger than a certain size. Or, you may want to set a minimum size. You can set this by using the CSS properties listed below.
These properties are only useful for flexible elements. By setting a maximum height, for example, a stretchy button will only grow to a certain maximum height. You will still be able to resize the window beyond that point but the button will stop growing in size. The box the button is inside will also continue to grow, unless you set a maximum height on the box also.
If two buttons are equally flexible, normally both will share the amount of extra space. If one button has a maximum width, the second will still continue to grow and take all of the remaining space.
If a box has a maximum width or height, the children cannot grow larger than that maximum size. If a box has a minimum width or height, the children cannot shrink smaller than that minimum size. Here are some examples of setting widths and heights:
<button label="1" style="width: 100px;"/> <button label="2" style="width: 100em; height: 10px;"/> <button label="3" flex="1" style="min-width: 50px;"/> <button label="4" flex="1" style="min-height: 2ex; max-height: 100px"/> <textbox flex="1" style="max-width: 10em;"/> <html style="max-width: 50px">This is some boring but simple wrapping text.</html>
Example 1: the first button will be displayed with a width of
100 pixels (px means pixels). You need to add the unit or the width will be
ignored.
Example 2: the second button will be displayed with a height
of ten pixels and a width of 100 ems (an em is the size of a character in the
current font).
Example 3: the third button is flexible so it will grow based
on the size of the box the button is in. However, the button will never shrink
to be less than 50 pixels. Other flexible components such as spacers will
absorb the remaining space, breaking the flex ratio.
Example 4: the fourth button is flexible and will never have
a height that is smaller than 2 ex (an ex is usually the height of the letter
x in the current font) or larger than 100 pixels.
Example 5: the text input is flexible but will never grow to
be larger than 10 ems. You will often want to use ems when specifying sizes
with text in them. This unit is useful for textboxes so that the font can
change and the textboxes would always be a suitable size, even if the font
is very large.
Example 6: the description element
is constrained to have a maximum width of 50 pixels. The text inside will wrap
to the next line, after fifty pixels.
Let's add some of these styles to the find files dialog. We'll make it so that the textbox will resize to fit the entire window.
<textbox id="find-text" flex="1" style="min-width: 15em;"/>Here, the text input has been made flexible. This way, it will grow if the user changes the size of the dialog. This is useful if the user wants to enter a long string of text. Also, a minimum width of 15 ems has been set so that the text box will always show at least 15 characters. If the user resizes the dialog to be very small, the text input will not shrink past 15 ems. It will be drawn as if it extends past the edge of the window. Notice in the image below that the text input has grown to extend to the full size of the window.
Let's say you have a box with two child elements, both of which are not flexible, but the box is flexible. For example:
Example 3.2.3: Source View<box flex="1"> <button label="Happy"/> <button label="Sad"/> </box>
If you resize the window, the box will stretch to fit the window size. The buttons are not flexible, so they will not change their widths. The result is extra space that will appear on the right side of the window, inside the box. You may wish, however, for the extra space to appear on the left side instead, so that the buttons stay right aligned in the window.
You could accomplish this by placing a spacer inside the box, but that gets messy when you have to do it numerous times. A better way is to use an additional attribute pack on the box. This attribute indicates how to pack the child elements inside the box. For horizontally oriented boxes, it controls the horizonal positioning of the children. For vertically oriented boxes, it controls the vertical positioning of the children. You can use the following values:
The pack attribute applies to the box containing the elements to be packed, not to the elements themselves.
We can change the earlier example to center the elements as follows:
Example 3.2.4: Source View<box flex="1" pack="center"> <button label="Happy"/> <button label="Sad"/> </box>
Now, when the window is resized, the buttons center themselves horizontally. Compare this behavior to that of the previous example.
If you resize the window in the Happy-Sad example above horizontally, the box will grow in width. If you resize the window vertically however, you will note that the buttons grow in height. This is because the flexibility is assumed by default in the other direction.
You can control this behavior with the align attribute. For horizontal boxes, it controls the position of the children vertically. For vertical boxes, it controls the position of the children horizontally. The possible values are similar to the pack.
As with the pack attribute, the align attribute applies to the box containing the elements to be aligned, not to the elements themselves.
For example, the first box below will have its children stretch, because that is the default. The second box has an align attribute, so its children will be placed centered.
Example 3.2.5: Source View<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window id="yesno" title="Question" orient="horizontal"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<hbox>
<button label="Yes"/>
<button label="No"/>
</hbox>
<hbox align="center">
<button label="Maybe"/>
<button label="Perhaps"/>
</hbox>
</window>
You can also use the style properties -moz-box-pack and -moz-box-align instead of specifying attributes.
You could potentially create a button element that contains a label that is larger than the maximum width of the button. Of course, a solution would be to increase the size of the button. However, buttons (and other elements with a label) have a special attribute called crop that allows you to specify how the text may be cropped if it is too big.
If the text is cropped, an ellipsis (...) will appear on the button where the text was taken out. Four possible values are valid:
This attribute is really only useful when a dialog has been designed to be useful at any size. The crop attribute can also be used with the description element and other elements that use the label attribute for labels. The following shows this attribute in use:
Example 3.2.6: Source View<button label="Push Me Please!" crop="right" flex="1"/>
Notice how the text on the button has had the right side of it cropped
after the window is made smaller.
We've seen a lot of features of the box model. Here, we'll find out some more details with some examples.
The style properties such as min-width and max-height can be applied to any element. We've added them to buttons and textboxes, but we can also add them to spacers or boxes. In addition, the flex attribute can be applied to any element.
Example 3.3.1: Source View<hbox flex="1"> <button label="Left" style="min-width: 100px;" flex="1"/> <spacer flex="1"/> <button label="Right" style="min-width: 100px;" flex="1"/> </hbox>
In the example above, all three elements will resize themselves as they are all flexible. The two buttons indicate a minimum width of 100 pixels. The buttons will never be smaller than this size but they may grow larger. Here the window should appear just over 200 pixels wide. That's enough for the two buttons. Because all three elements are flexible, but they don't need any more room, the flexibility adds no extra space.
As shown in the image above, there are two buttons which expand vertically to fit their container, which in this case is the hbox. The align attribute controls this behavior on a horizontal box. You can also prevent this strecthing by placing a maximum height on the elements or, better, on the box itself. If a box has a maximum height, the elements inside it are constrained by this. However, the problem with this is that you need to know how big the element will be beforehand. The following shows the example with a align attribute set.
Example 3.3.2: Source View<hbox flex="1" align="top"> <button label="Left" style="min-width: 100px;" flex="1"/> <spacer flex="1"/> <button label="Right" style="min-width: 100px;" flex="1"/> </hbox>
To achieve complicated layouts, you will generally need to add nested boxes, specify minimum and maximum sizes on some elements, and make certain elements flexible. The best interface is one that can be displayed at various sizes without problems. The box model may be difficult to understand without trying various various things out for yourself.
The following is an outline of both types of boxes:
You can put boxes anywhere in a XUL file, including inside an HTML element such as a table. However, the layout will be partly controlled by the HTML element. That means that the flex might not work exactly as you want it. Remember that flexibility only has meaning for elements that are directly inside a box or an element that is a type of box.
1. Using Spacers
Example 3.3.3: Source View<hbox> <button label="One"/> <spacer style="width: 5px"/> <button label="Two"/> </hbox>
Here, a spacer is used as a separator between the two buttons, by setting an explicit width of 5 pixels. You could also set margins (using the CSS margin property).
2. Centering Buttons
Example 3.3.4: Source View<hbox pack="center" align="center" flex="1"> <button label="Look at Me!"/> <button label="Push Me!"/> </hbox>
This example contains a horizontal box with two buttons in it, contained inside a flexible box. The box has the pack attribute which is used to center the buttons horizontally. The align attribute aligns the buttons vertically. The result is that the buttons will be centered in the box in both directions. This example will work with a vertical box as well, although the second button will be underneath the first one, instead of beside it.
3. A Find Text Dialog
Example 3.3.5: Source View<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window id="findtext" title="Find Text" orient="horizontal"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<vbox flex="3">
<label control="t1" value="Search Text:"/>
<textbox id="t1" style="min-width: 100px;" flex="1"/>
</vbox>
<vbox style="min-width: 150px;" flex="1" align="start">
<checkbox id="c1" label="Ignore Case"/>
<spacer flex="1" style="max-height: 30px;"/>
<button label="Find"/>
</vbox>
</window>Here, two vertical boxes are created, one for the textbox and the other for the check box and button. The left box has a flexiblity that is 3 times greater than the right one so it will always receive 3 times as much of the extra space when the window size is increased. The right box enforces a minimum width of 150 pixels.
The textbox is flexible so it will resize as the window resizes. The textbox also enforces a minimum width of 100 pixels. The check box appears in the right box along with its label. Just below the check box is a spacer. The spacer will grow and shrink but not exceed 30 pixels. The result is that the check box and the Find button will be spaced apart from each other by some space of no more than 30 pixels.
The second box has an alignment of start. This causes the child elements to be aligned on the left edge. If this was not specified, the default would be stretch, which would make the child elements stretch horizontally. Because we don't want the Find button to change size, we need to set an alignment.
This section describes a way to include elements into groups.
HTML provides an element, fieldset which can be used to group elements together. A border is typically drawn around the elements to show that they are related. An example would be a group of check boxes. XUL provides an equivalent element, groupbox which can be used for a similar purpose.
As its name implies, the groupbox is a type of box. That means that the elements inside it align themselves according to the rules of boxes. There are two differences between groupboxes and regular boxes:
Because groupboxes are types of boxes, you can use the same attributes such as orient and flex. You can put whatever elements you want inside the box, although typically they will be related in some way.
The label across the top of the groupbox can be created by using the caption element. It works much like the HTML legend element. A single caption element placed as the first child is sufficient.
The example below shows a simple groupbox:
Example 3.4.1: Source View<groupbox> <caption label="Answer"/> <description value="Banana"/> <description value="Tangerine"/> <description value="Phone Booth"/> <description value="Kiwi"/> </groupbox>
This will cause four pieces of text to be displayed surrounded by a box with
the label Answer. Note that the groupbox has a vertical orientation by default
which is necesssary to have the text elements stack in a single column.
You can also add child elements inside the caption element to create a more complex caption. For example, Mozilla's Font preferences panel uses a drop-down menu as a caption. Although any content can be used, usually you would use a checkbox or dropdown menu.
Example 3.4.2: Source View<groupbox flex="1">
<caption>
<checkbox label="Enable Backups"/>
</caption>
<hbox>
<label control="dir" value="Directory:"/>
<textbox id="dir" flex="1"/>
</hbox>
<checkbox label="Compress archived files"/>
</groupbox>
In this example, a checkbox has been used as a caption. We might use a script
to enable and disable the contents of the groupbox when the checkbox is checked
and unchecked. The groupbox contains a horizontal box with a label and textbox.
Both the textbox and groupbox have been made flexible so the textbox expands when
the window is expanded. The additional checkbox appears below the textbox because
of the vertical orientation of the groupbox. We'll add a groupbox to the find
files dialog in the next section.
You can use the radiogroup element to group radio elements together. The radiogroup is a type of box. You can put any element you want inside it, and apart from its special handling of radio buttons, it works like any other box.
Any radio buttons placed inside the radio group will be grouped together, even if they are inside nested boxes. This could be used to add extra elements within the structure, such as in the following example:
Example 3.4.3: Source View<radiogroup>
<radio id="no" value="no" label="No Number"/>
<radio id="random" value="random" label="Random Number"/>
<hbox>
<radio id="specify" value="specify" label="Specify Number:"/>
<textbox id="specificnumber"/>
</hbox>
</radiogroup>We'll conclude the discussion of boxes by adding some boxes to the find files dialog.
We'll add some more elements to the find files dialog now. First, we'll add the capability to search for other information such as the file size and date.
<hbox>
<menulist id="searchtype">
<menupopup>
<menuitem label="Name"/>
<menuitem label="Size"/>
<menuitem label="Date Modified"/>
</menupopup>
</menulist>
<spacer style="width: 10px;"/>
<menulist id="searchmode">
<menupopup>
<menuitem label="Is"/>
<menuitem label="Is Not"/>
</menupopup>
</menulist>
<spacer style="width: 10px;"/>
<textbox id="find-text" flex="1" style="min-width: 15em;"/>
</hbox>Two drop down boxes have been added to the dialog. A spacer has been added in-between each element to separate them. These spacers have been given an explicit width of 10 pixels each. You'll notice that if the window is resized, the textbox grows but the other components do not. You'll also notice that the label was removed.
If you resize the window vertically, the elements do not change size. This is because they are inside horizontal boxes. In might be more appropriate if the Find and Cancel buttons always stayed along the bottom of the window. This is easy to do by adding a spacer in-between the two horizontal boxes.
<spacer style="height: 10px"/> <hbox> <menulist id="searchtype"> <menupopup> <menuitem label="Name"/> <menuitem label="Size"/> <menuitem label="Date Modified"/> </menupopup> </menulist> <spacer style="width: 10px;"/> <menulist id="searchmode"> <menupopup> <menuitem label="Is"/> <menuitem label="Is Not"/> </menupopup> </menulist> <spacer style="width: 10px;"/> <textbox id="find-text" flex="1" style="min-width: 15em;"/> </hbox> <spacer style="height: 10px" flex="1"/> <hbox>
Now when the dialog is resized, the two buttons will move so that they are always along the bottom of the dialog. The first spacer adds extra spacing in-between the title label and the search criteria elements.
It might look nicer if there was a border around the search criteria. There are two ways to do this. We could use the CSS border property or we could use the groupbox element. This first method would require that we set the style on the box itself. We'll use the latter method, however. A groupbox has the advantage that it draws a box with a nice beveled look, suitable for the current theme.
Let's change the box into a groupbox:
<groupbox orient="horizontal"> <caption label="Search Criteria"/> <menulist id="searchtype"> . . . <spacer style="width: 10px;"/> <textbox id="find-text" flex="1" style="min-width: 15em;"/> </groupbox>
There are other cosmetic problems as well. We could also have the groupbox grow so that it extends vertically to the bottom of the box. Also, we could modify some of the margins so that the elements are positioned better.
We'll see more examples of the box model and some of its features as we continue to add elements throughout the tutorial.
In this section, we'll look at creating progress meters.
A progress meter is a bar that indicates how much of a task has been completed. You typically see it when downloading files or when performing a lengthy operation. XUL has a progress meter element which can be used to create these. There are two types of progress meters: determinate and undeterminate.
Determinate progress meters are used when you know the length of time that an operation will take. The progress meter will fill up and, once full, the operation should be finished. This can be used for the download file dialog as the size of the file is known.
Undeterminate progress meters are used when you do not know the length of time of an operation. In Mozilla, the progress meter will look like a spinning barber pole, and will animate like one.
Determinate progress meter:
Undeterminate progress meter:
The progress meter has the following syntax:
<progressmeter
id="identifier"
mode="determined"
value="0%"/>The attributes are as follows:
Let's add a progress meter to our find file dialog. We would normally put an undeterministic progress meter as we don't know how many files we'll be searching or how long the search will take. However, we'll add a normal one for now as an animating one can be distracting during development. The progress meter would normally only appear while the search is taking place. We'll add a script later to turn it on and off.
<hbox>
<progressmeter value="50%" style="margin: 4px;"/>
<spacer flex="1"/>
The value has been set to 50% so that we can see the meter on the window. A margin has been set to 4 pixels so that it is separated from the edge of the window. As was stated earlier, we only want the progress bar to be displayed while the search was occuring. A script will show and hide it as necessary.
Now, let's find out to add scroll bars to a window.
A scroll bar is typically used so that a user can move around in a large document. You can also use it when you need to ask for a value that falls within a certain range. Scroll bars can be created in a number of ways. In XUL, one can be created using the scrollbar tag. Some elements, such as text boxes, will also add scroll bars as necessary when the content inside is too large.
First, we'll discuss creating a stand-alone scroll bar. The user will set the value by adjusting the scroll bar. You probably won't use this too often. A scroll bar is made up of several parts, the slider, which is the main part of the scroll bar with the adjustable box, and the two arrow buttons on the end. A scroll bar creates all of these elements automatically.
The syntax of a scroll bar is as follows:
<scrollbar
id="identifier"
orient="horizontal"/>
curpos="20"
maxpos="100"
increment="1"
pageincrement="10"/>The attributes are as follows:
The example given in the syntax above will create a scroll bar that can range from a value of 0 to 100. The value 100 might be the number of lines in a list, but it could be anything you want. The initial value in this example is 20. When clicking on one of the scroll bar arrows, the value would change by 1 either up or down. By paging through the scroll bar, the value will change by 10.
When the user clicks the scroll bar arrows, the thumb will move by the amount specified by the value increment. Increasing the value of this attribute will cause the scroll bar to move farther with each click. The leftmost or topmost position of the scroll bar has the value 0 and the rightmost or bottommost position of the scroll bar has the value given by maxpos.
By adjusting the values of the scroll bar, you can have the thumb positioned just as you want it and the change when the user clicks the arrows just as you want it.
There may be need to display elements as a set of overlapping cards. The stack and deck elements can be used for this purpose.
Each XUL box is a container that can contain any other element. There are a number of elements that are specialized types of boxes, such as toolbars and tabbed panels. The box tag creates the simplest box with no special properties. However, the specialized types of boxes work just like regular boxes in the way they orient the elements inside them, but they have additional features.
In fact, many components can contain other elements. We've already seen that buttons may contain other things besides the default. A scroll bar is just a special type of box that creates its own elements if you don't provide them. It also handles the moving of the scroll bar thumb.
In the next few sections, we'll introduce some elements that are designed for holding other elements. They are all special types of boxes and allow all of the attributes of boxes on them.
The stack element is a simple box. It works like any other box but has the special property that its children are laid out all on top of each other. The first child of the stack is drawn underneath, the second child is drawn next, followed by the third and so on. Any number of elements may be stacked up in a stack.
The orient property has little meaning on a stack as children are laid out above each other rather than from side to side. The size of the stack is determined by its largest child, but you can use the CSS properties width, height, min-width and other related properties on both the stack and its children.
The stack element might be used for cases where a status indicator needs to be added over an existing element. The Mozilla progress bar is made up of two elements, a bar and a label, created with a stack.
One convenient use of the stack element however is that you could emulate a number of CSS properties with it. For example, you could create an effect similar to the text-shadow property with the following:
Example 4.3.1: Source View<stack> <description value="Shadowed" style="padding-left: 1px; padding-top: 1px; font-size: 15pt"/> <description value="Shadowed" style="color: red; font-size: 15pt;"/> </stack>
Both description elements create text with a size of 15 points. The first, however is offset one pixel to the right and down by adding a padding to its left and top sides. This has the result of drawing the same text 'Shadowed' again but slightly offset from the other. The second description element is drawn in red so the effect is more visible.
This method has advantages over using text-shadow because you could completely style the shadow apart from the main text. It could have its own font, underline or size. (You could even make the shadow blink). It also useful as Mozilla doesn't currently support CSS text shadowing. A disadvantage is that the area taken up by the shadow makes the size of the stack larger. Shadowing is very useful for creating the disabled appearance of buttons:
Example 4.3.2: Source View<stack style="background-color: #C0C0C0"> <description value="Disabled" style="color: white; padding-left: 1px; padding-top: 1px;"/> <description value="Disabled" style="color: grey;"/> </stack>
This arrangement of text and shadow colors creates the disabled look under some platforms.
Note that events such as mouse clicks and keypresses are passed to the element
on the top of the stack, that is, the last element in the stack. That means that
buttons will only work properly as the last element of the stack.
A deck element also lays out its children on top of each other much like the stack element, however decks only display one of their children at a time. This would be useful for a wizard interface where a series of similar panels are displayed in sequence. Rather than create separate windows and add navigation buttons to each of them, you would create one window and use a deck where the content changes.
Like stacks, the direct children of the deck element form the pages of the deck. If there are three children of the deck element, the deck will have three children. The displayed page of the deck can be changed by setting an selectedIndex attribute on the deck element. The index is a number that identifies which page to display. Pages are numbered starting from zero. So, the first child of the deck is page 0, the second is page 1 and so on.
The following is an example of a deck:
Example 4.3.3: Source View<deck selectedIndex="2">
<description value="This is the first page"/>
<button label="This is the second page"/>
<box>
<description value="This is the third page"/>
<button label="This is also the third page"/>
</box>
</deck>Three pages exist here, the default being the third one. The third page is a box with two elements inside it. Both the box and the elements inside it make up the page. The deck will be as large as the largest child, which here should be the third page.
You can switch pages by using a script to modify the selectedIndex attribute. More on this in the section on events and the DOM.
This section will describe how to position items in a stack.
Normally, the child elements of a stack stretch to fit the size of the stack. However, you may also place the children at specific coordinates. For example, if a stack has two buttons as children, one may be placed 20 pixels from the left edge and 50 pixels from the top edge. The second button can be placed at 100 pixels from the left edge and 5 pixels from the top edge.
The position of a child element may be specified by placing two attributes on the element. For the horizontal position, use the left attribute and for the vertical position, use the top attribute. If you don't put these attributes on a child of a stack, the child will stretch to fit the size of the stack.
Example 4.4.1: Source View<stack> <button label="Goblins" left="5" top="5"/> <button label="Trolls" left="60" top="20"/> <button label="Vampires" left="10" top="60"/> </stack>
The stack here contains three elements,
each positioned at the coordinates given by the
left and
top attributes. Here, all three children are
buttons, but the elements do not have to be same type. They may be any
element, including boxes and other stacks.
The size of a stack is determined by the positions of the child elements. It is always sized so that all of the child elements are visible. So if you set a left attribute to 400, the stack will have a width around 400 pixels plus the width of the element. You can override this size with the various style properties such as width and max-width.
You can use a script to adjust the value of the left and top attributes and thus make the elements move around. Stacks have the advantage that when one absolutely positioned element changes its position, the position of the other elements is not affected. If you tried to move elements in a regular box, other elements might shuffle their positions around.
It is also possible to place the child elements so that they overlap. When drawing the child elements, the elements are shown in the order that they appear in the stack. That is, the first child of the stack appears at the back, the next child appears next and so on. The last element appears on top. You can use the DOM functions to move the order of the elements around.
When responding to mouse events, the elements on top will capture the events first. That means that if two buttons overlap, the top button will capture a mouse click where it covers the other one.
It is common in preference dialogs for tabbed pages to appear. We'll find out how to create them here.
Tabboxes are typically used in an application in the preferences window. A series of tabs appears across the top of a window. The user can click each tab to see a different set of options. It is useful in cases when you have more options than will fit in one screen.
XUL provides a method to create such dialogs. It involves five new elements, which are described briefly here and in more detail below.
The tabbox is the outer element. It consists of two children, a tabs element which contains the row of tabs and a tabpanels elements which contains the tabbed pages.
Shown below is the typical syntax of a basic tabbox with two tabs:
<tabbox id="tablist">
<tabs>
-- tab elements go here --
</tabs>
<tabpanels>
-- tabpanel elements go here --
</tabpanels>
</tabbox>The tab elements are placed inside a tabs element which is much like a regular box. The tabs element itself has been placed inside a tabbox. The tabbox also contains a tabpanels element which will appear below the tabs due to the vertical orientation on the whole tabbox.
There is really nothing special about the tab elements that make them different than boxes. The difference is that the tabs render slightly differently and only one tab panel's contents are visible at once, much like with a deck.
The content of the individual tab pages should go inside each tabpanel element. It does not go in the tab element as that is where the contents of the tab along the top go (which could contain any elements itself).
Each tabpanel element becomes a page on the tabbed display. The first panel corresponds to the first tab, the second element inside the panel corresponds to the second element, and so on. There is a one-to-one relationship between each tab element and the tabpanel elements.
When determining the size of the tabbox, the size of the largest page is used. That means that if there are ten textboxes on one tab page and only one on another, the tab page will be sized to fit the one with the ten on it as this takes up more room. The area taken up by the tab area does not change when the user switches to a new tab page.
Let's look at an example:
Example 4.5.1: Source View<tabbox>
<tabs>
<tab label="Mail"/>
<tab label="News"/>
</tabs>
<tabpanels>
<tabpanel id="mailtab">
<checkbox label="Automatically check for mail"/>
</tabpanel>
<tabpanel id="newstab">
<button label="Clear News Buffer"/>
</tabpanel>
</tabpanels>
</tabbox>
Here, two tabs have been added. The first one labeled Mail and the other labeled
News. When the user clicks the Mail tab, the contents of the first tab page will
be displayed. In this case, the box with the check box labeled 'Automatically
check for mail' will appear in the first tab. The second tab, when clicked, will
display the box containing the button labeled Clear News Buffer. In the code, the
two tab pages have been labeled 'Mail' and 'News'.
The currently selected tab element is given an additional selected attribute on a tab element which is set to true. This is used to give the currently selected tab a different appearance so that it will look selected. Only one tab will have a true value for this attribute at a time.
Finally, you can change the position of the tabs so that they appear on any side of the tab pages. There is no special syntax to do this. You simply rearrange the position of the tabs and set the orient attribute as necessary. Remember that the tab elements are much like regular boxes in terms of layout. However, you should probably leave the tabs on top, otherwise they might not look very good under particular themes.
For example, to put the tabs along the left side, change the orientation of the tabs so that it is vertical. This is because you want the tabs to appear vertically stacked. Also, adjust the tabbox so it has horizontal orientation. This needs to be done because you want the tabs to appear beside the tab pages.
You can place the tabs on the right or bottom side by moving the tabs so that it is after the tabpanels.
Let's add a second panel to the find files dialog. We'll create an Options tab that will contain some options for searching. This may not be the best interface for doing this, but we'll use it to demonstrate tabs. The label across the top and the search criteria box will need to go on the first tab. We'll add some options on the second tab. The progress bar and the buttons can stay on the main dialog, outside of the tabs.
<vbox flex="1"> <tabbox> <tabs> <tab label="Search" selected="true"/> <tab label="Options"/> </tabs> <tabpanels> <tabpanel id="searchpanel" orient="vertical"> <description> Enter your search criteria below and select the Find button to begin the search. </description> <spacer style="height: 10px"/> <groupbox orient="horizontal"> <caption label="Search Criteria"/> <menulist id="searchtype"> <menupopup> <menuitem label="Name"/> <menuitem label="Size"/> <menuitem label="Date Modified"/> </menupopup> </menulist> <spacer style="width: 10px;"/> <menulist id="searchmode"> <menupopup> <menuitem label="Is"/> <menuitem label="Is Not"/> </menupopup> </menulist> <spacer style="height: 10px"/> <textbox id="find-text" flex="1" style="min-width: 15em;"/> </groupbox> </tabpanel> <tabpanel id="optionspanel" orient="vertical"> <checkbox id="casecheck" label="Case Sensitive Search"/> <checkbox id="wordscheck" label="Match Entire Filename"/> </tabpanel> </tabpanels> </tabbox>
The tab elements have been placed around the main content of the window. You can see the two tabs, Search and Options. Clicking on each one will bring up the respective tab pages. As shown by the image, the two options appear on the second tab. The first tab looks pretty much like it did before, apart from the tabs along the top.
A toolbar is usually placed along the top of a window and contains a number of buttons that perform common functions. XUL has a method to create toolbars.
Like a number of elements, XUL toolbars are a type of box. Usually, a row of buttons would appear in the toolbar, but any element can be placed in a toolbar. For example, the Mozilla browser window contains a textbox that displays the page URL.
Toolbars may be placed on any side of the window, either horizontally or vertically. Of course you wouldn't normally put a textbox in a vertical toolbar. Actually, because toolbars are just boxes they can actually go anywhere you want, even in the middle of a window. Typically, however, a set of toolbars would appear along the top of a window. When more than one toolbar is placed next to each other, they are typically grouped together in something called a toolbox.
Along the left side of the toolbar is a little notch which, if clicked, will collapse the toolbar so that only the notch is visible. The notch is called a grippy. When mutliple toolbars are all placed in the same toolbox, the grippies will collapse into a single row. This shrinks the total amount of space that is used. Vertical toolbars have their grippies along their top edge. The user will usually collapse the toolbar if they want more space for the main window.
Here is a example of a simple toolbar inside a toolbox.
Example 4.6.1: Source View<toolbox>
<toolbar id="nav-toolbar">
<toolbarbutton label="Back"/>
<toolbarbutton label="Forward"/>
</toolbar>
</toolbox>
This has created a toolbar containing two buttons, a Back button and a Forward
button. The one toolbar has been placed inside the toolbox. This has involved
four new tags, which are described here.
The toolbar is the main element that creates the actual toolbar. Inside it are placed the individual toolbar items, usually buttons, but they can be other elements. The toolbar should have an id attribute or the grippy won't be able to collapse or expand the toolbar properly.
In the example above, only one toolbar was created. Multiple toolbars can be created just as easily by adding more toolbar elements after the first one.
The toolbox is a container for toolbars. In some applications, you will have several toolbars along the top of the window. You can put them all inside a toolbox.
You do not have to put toolbar elements inside a toolbox.
The grippies on the toolbox are created using another element, a toolbargrippy. It doesn't really make any sense to use it outside of a toolbar as it will have no special purpose as it will have nothing to collapse. However, you may wish to style it differently. You can hide the grippy by adding the grippyhidden attribute to the toolbar element, set to the value true.
A toolbox with three toolbars in it:
The same set of toolbars but two have been collapsed.
Let's add a toolbar to the find files dialog. We don't really need one but we'll add one anyway to demonstrate its use. Two buttons will be added, an Open button and a Save button. Presumably, they would allow the user to save search results and re-open them later.
<vbox flex="1">
<toolbox>
<toolbar id="findfiles-toolbar">
<toolbarbutton id="opensearch" label="Open"/>
<toolbarbutton id="savesearch" label="Save"/>
</toolbar>
</toolbox>
<tabbox>A toolbar with two buttons has been added here. In the image, you can see them appear horizontally along the top. The grippy also appears on the left side of the toolbar. Notice that the toolbar has been placed inside the vertical box just above the tabbox. This is because we need the vertical orientation so that the toolbar will appear above everything else.
In this section, we'll look at how to add panels that can display HTML pages or other XUL files.
There may be times when you want to have part of a document loaded from a different page. Sometimes, you will want to change part of the window. A good example is a step-by-step wizard that guides you through a number of screens, asking a set of questions. Each time the user clicks the Next button, the next screen of the wizard is displayed.
You could create a wizard interface by opening a different window for each screen. There are three problems with this approach. First, each window could appear in a different location (although there are ways around this). Second, the elements such the Back and Next buttons are the same throughout the interface. It would be much better if just the content area of the wizard changed. Third, it would be difficult to co-ordinate scripts when running in different windows.
A better approach is to use the iframe element, which works much like the HTML element of the same name. It creates a separate document within the window. It has the advantage that it can be placed anywhere and the contents can be loaded from a different file. Set the URL to appear in the frame with the src attribute. This URL may point to any kind of file, although it will usually point to an HTML file or another XUL file. You can use a script to change the contents of the iframe without affecting the main window.
In the Mozilla browser window, the area where the web page is displayed is created by using an iframe. When the user enters a URL or clicks on a link in a document, the source of the frame is changed.
The following is an example of using an iframe:
Example 4.7.1: Source View<toolbox>
<toolbar id="nav-toolbar">
<toolbarbutton label="Back"/>
<toolbarbutton label="Forward"/>
<textbox id="urlfield"/>
</toolbar>
</toolbox>
<iframe id="content-body" src="http://www.mozilla.org" flex="1"/>The example here has created a very simple interface for a web browser, although it isn't functionally complete. A box has been created containing two elements: a toolbox and an iframe. A Back button, a Forward button and a field for typing is URLs has been added to the only toolbar. The web pages would appear inside the iframe. In this case, the file welcome.html would appear by default.
Here, we'll look at how to add splitters to a window.
There may be times when you want to have two sections of a window where the user can resize the sections. An example is the Mozilla browser window, where you can change the size of the sidebar panel by dragging the bar in-between the two frames. You can also hide the sidebar by clicking the notch.
This feature is accomplished by using an element called a splitter. It creates a skinny bar between two sections which allows the sides to be resized. You can place a splitter anywhere you want and it will allow resizing of the elements that come before it and the elements that come after it in the same box.
When a splitter is placed inside a horizontal box, it will allow resizing horizontally. When a splitter is placed inside a vertical box, it will allow resizing vertically.
The syntax of a splitter is as follows:
<splitter
id="identifier"
state="open"
collapse="before"
resizebefore="closest"
resizeafter="closest">The attributes are as follows:
If you set the collapse attribute, you should also add a grippy element inside the splitter which the user can use to collapse the element.
An example would be helpful here:
Example 4.8.1: Source View<box>
<iframe id="content-1" width="60" height="20" src="w1.html"/>
<splitter collapse="before" resizeafter="farthest">
<grippy/>
</splitter>
<iframe id="content-2" width="60" height="20" src="w2.html"/>
<iframe id="content-3" width="60" height="20" src="w3.html"/>
<iframe id="content-4" width="60" height="20" src="w4.html"/>
</box>
Here, four iframes have been created and a splitter has been placed
in-between the first and second one. The collapse
has been set to a value of before, meaning that if
the splitter grippy is clicked on, the first frame would disappear and the
splitter and the remaining frames would shuffle to the left. Ths splitter
grippy is drawn centered inside the splitter.
The splitter has been given a resizeafter value of farthest. This means that when the splitter is dragged, the farthest element after it will change size. In this case, frame 4 will change size.
A value has not been specified for resizebefore so it will default to a value of closest. In this case, there is only one frame before the splitter, so frame 1 will change size.
Frames 2 and 3 will only change size if you drag the splitter far enough to the right that frame 4 has reached its minimum size.
The 4 panels with the splitter in a collapsed state:
An image of the 4 panels with the splitter resized to the right is shown below. Notice how the middle two panels have not changed size. Only panel 1 and panel 4 have changed size. You can just see part of the fourth panel. If you continue to drag the splitter to the right, the other two panels will shrink.
You can use the style properties such as min-width, max-height on the iframes to specify minimum or maximum widths or heights in the box. If you do, the splitter will detect this and not allow the user to drag the splitter past the minimum and maximum sizes.
For example, if you specified a minimum width of 30 pixels on panel 4 above, it would not shrink below that size. The other two panels would have to shrink. If you set the minimum width of panel 1 to 50 pixels, you would only be able to drag the splitter 10 pixels to the left (as it starts at 60 pixels wide). You can still collapse the splitter however.
You can also place more than one splitter in a box if you want, in which case you could collapse different parts of it. Similarly, you do not have to collapse just iframes. Any element can be collapsed.
Let's see what the find file dialog looks like with a splitter in it. One possibility would be to add the results of the search in the dialog. We'll add an area in-between the search criteria and the buttons along the bottom. A splitter will allow you to collapse, or hide, the search results.
</tabbox>
<iframe src="results.html"/>
<splitter resizeafter="grow"/>
<hbox>Here, a splitter and an iframe have been added to the dialog. We don't need the spacer after the tabbox any more so we can remove it. The content of the frame is contained in a file called 'results.html'. Create this file and put whatever you want in it for now. The iframe will be replaced later with a results list when we know how to create it. For now, it serves to demonstrate the splitter.
The splitter has been set to a collapse value of before meaning that the element just before the splitter will collapse. Here, it is the iframe. As the images below show, when the grippy is clicked, the iframe is collapsed and the buttons shuffle up.
The resizeafter attribute has been set to grow so that the elements after the splitter push themselves down when the splitter is dragged down. This results in the content of the frame growing to any size. It should be noted that the window does not resize itself automatically. You'll also notice that this is a horizontal splitter because it has been placed in a vertical box.
Normal State:
Collapsed State:
This section describes how to save the state of a XUL window.
When building a large application, you will typically want to be able to save some of the state of a window across sessions. For example, the window should remember which toolbars are collapsed even after the user exits.
One possibility would be to write a script to collect information about what you would like to save and then save it to a file. However, that would be a pain to do for every application. Conveniently, XUL provides such a mechanism to save the state of a window.
The information is collected and stored in a RDF file (localstore.rdf) in the same directory as other user preferences. It holds state information about each window. This method has the advantage that it works with Mozilla user profiles, so that each user can have different settings.
XUL allows you to save the state of any element. You will typically want to save toolbar states, window positions and whether certain panels are displayed or not, but you can save almost anything.
To allow the saving of state, you simply add a persist attribute to the element which holds a value you want to save. The persist attribute should be set to a space-separated list of attributes of the element that you want to save.
For example, to save the position of a window, you would do the following:
<window width="200" height="300" persist="width height" . . .
The two attributes of the window element, the width and the height will be saved. You could add additional attributes by adding a space and another attribute name to the persist attribute. You can add the persist attribute to any element and store any attribute. You might use unusual values if you adjust attributes using a script.
Let's add the persist attribute to some of the elements in the find files dialog. To save the position of the window. To do this, we need to modify the window.
<window
id="findfile-window"
title="Find Files"
persist="screenX screenY width height"
orient="horizontal"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">This will cause the x and y position of the window and the width and height of the window to be saved. We could extend it further to save the collapsed state of the splitter. It doesn't really make sense to save the current tab state.
In this section, we'll see how to create a menu bar with menus on it.
XUL has a number of different ways of creating menus. The most basic way is to add a menu bar with a row of menus on it like many applications have. You can also create popup menus. The menu features of XUL consist of a number of different elements which allow you to create menu bars or popup menus. The items on the menus can be customized quite easily. We've already seen part of how to make menus using the menulist. This section will build on that.
Menu bars are usually created much like a toolbar. The menu bar can be placed inside a toolbox and a grippy will appear on its left side so that it can be collapsed. The menu would work just like any other toolbar. XUL does have some special menu elements which provide special functionality typical of menus.
There are five elements associated with creating a menu bar and its menus, which are explained briefly here and in detail afterwards:
You can customize the menus on the menubar to have whatever you want on them on all platforms except the Macintosh. This is because the Macintosh has its own special menu along the top of the screen controlled by the system. Although you can create custom menus, any special style rules or non-menu elements that you place on a menu may not be applied. You should keep this is mind when creating menus.
Here is an example of a simple menu bar:
Example 5.1.1: Source View<toolbox flex="1">
<menubar id="sample-menubar">
<menu id="file-menu" label="File">
<menupopup id="file-popup">
<menuitem label="New"/>
<menuitem label="Open"/>
<menuitem label="Save"/>
<menuseparator/>
<menuitem label="Exit"/>
</menupopup>
</menu>
<menu id="edit-menu" label="Edit">
<menupopup id="edit-popup">
<menuitem label="Undo"/>
<menuitem label="Redo"/>
</menupopup>
</menu>
</menubar>
</toolbox>
Here, a simple menu bar is created using the menubar
element. It will create a row for menus to be placed on. Two menus, File and
Edit have been created here. The menu element
creates the title at the top of the menu, which appears on the menu bar.
The popups are created using the menupoup element.
It will pop up when the user clicks on the parent menu title. The size of the
popup will be large enough to fit the commands inside it. The commands
themselves are created using the menuitem element.
Each one represents a single command on the menu popup.
You can also create separators on the menus using the menuseparator element. This is used to separate groups of menuitems.
The menubar is a box containing menus. Note that it has been placed inside a flexible toolbox. The menubar has no special attributes but it is a type of box. This means that you could create a vertical menubar by setting the orient attribute to vertical.
The menu element is normally placed on a menubar,
although it does not have to be. However, it will be given a different look.
The image here shows what the earlier example would look like without the
menu bar.
The menu element works much like the button element. It accepts some of the same attributes plus some additional ones:
The menupopup element creates the popup window containing the menu commands. It is a type of box which defaults to a vertical orientation. You could change it to horozontal if you wanted to and the menuitems would be placed in a row. Normally only menuitems and menuseparators would be placed on a menupopup. You can place any element on a menupopup, however they will be ignored on a Macintosh.
The menuitem element is much like the menu element and has some of the same attributes.
The menuseparator has no special attributes. It just creates a horizontal bar between the menuitems next to it.
In this section, we'll look at creating submenus and checked menus
You can create submenus inside other menus (nested menus) using the existing elements. Remember that you can put any element inside a menupopup. We've looked at placing menuitems and menuseparators in menupopups. However, you can create submenus by simply placing the menu element inside the menupopup element. This works because the menu element is valid even when it isn't directly placed inside a menu bar.
The example below creates a simple submenu inside the File menu:
Example 5.2.1: Source View<toolbox flex="1">
<menubar id="sample-menubar">
<menu id="file-menu" label="File">
<menupopup id="file-popup">
<menu id="new-menu" label="New">
<menupopup id="new-popup">
<menuitem label="Window"/>
<menuitem label="Message"/>
</menupopup>
</menu>
<menuitem label="Open"/>
<menuitem label="Save"/>
<menuseparator/>
<menuitem label="Exit"/>
</menupopup>
</menu>
</menubar>
</toolbox>
Let's add a menu to the find files dialog. We'll just add a few simple commands to a File menu and an Edit menu. This is similar to the example above.
<toolbox>
<menubar id="findfiles-menubar">
<menu id="file-menu" label="File" accesskey="f">
<menupopup id="file-popup">
<menuitem label="Open Search..." accesskey="o"/>
<menuitem label="Save Search..." accesskey="s"/>
<menuseparator/>
<menuitem label="Close" accesskey="c"/>
</menupopup>
</menu>
<menu id="edit-menu" label="Edit" accesskey="e">
<menupopup id="edit-popup">
<menuitem label="Cut" accesskey="t"/>
<menuitem label="Copy" accesskey="c"/>
<menuitem label="Paste" accesskey="p" disabled="true"/>
</menupopup>
</menu>
</menubar>
<toolbar id="findfiles-toolbar>
Here we have added two menus with various commands on them. Notice how the menu bar was added inside the toolbox. In the image, you can see the grippy on the menu bar that can be used to collapse the menu bar. The three dots after Open Search and Save Search are the usual way that you indicate to the user that a dialog will open when selecting the command. Access keys have been added for each menu and menu item. You will see in the image that this letter has been underlined in the menu label. Also, the Paste command has been disabled. We'll assume that there's nothing to paste.
Many applications have menu items that have checks on them. For example, a feature that is enabled has a check placed beside the command and a feature that is disabled has no check. When the user selects the menu, the check state is switched. You may also want to create radio buttons on menu items.
The checks are created in a similar way to the checkbox and radio elements. This involves the use of two attributes, type to indicate the type of check and name to group commands together. The example below creates a menu with a checked item.
Example 5.2.2: Source View<toolbox>
<menubar id="options-menubar">
<menu id="options_menu" label="Options">
<menupopup>
<menuitem id="backups" label="Make Backups" type="checkbox"/>
</menupopup>
</menu>
</menubar>
</toolbox>The type attribute has been added which is used to make the menu item checkable. By setting its value to checkbox, the menu item can be checked on and off by selecting the menu item.
In addition to standard checks, you can create the radio style of checks by setting the type to a value of radio. A radio check is used when you want a group of menu items where only one item can be checked at once. An example might be a font menu where only one font can be selected at a time. When another item is selected, the previously selected item is unchecked.
In order to group a set of menu items together you need to put a name attribute on each one to group. Set the value to the same string. The example below demonstrates this:
Example 5.2.3: Source View<toolbox>
<menubar id="planets-menubar">
<menu id="planet-menu" label="Planet">
<menupopup>
<menuitem id="jupiter" label="Jupiter" type="radio" name="ringed"/>
<menuitem id="saturn" label="Saturn" type="radio" name="ringed"/>
<menuitem id="uranus" label="Uranus" type="radio" name="ringed"/>
<menuitem id="earth" label="Earth" type="radio" name="inhabited"/>
</menupopup>
</menu>
</menubar>
</toolbox>If you try this example, you'll find that of the first three menu items, only one can be checked. They are grouped together because they all have the same name. The last menu item, Earth, is not part of the group because it has a different name. However, it is a radio button.
Of course, the grouped items all have to be within the same menu. They don't have to be placed next to each other in the menu, although it doesn't make as much sense if they don't.
In the last section, we looked at creating a menu on a menu bar. XUL also has the capability of creating popup menus. Popup menus are typically displayed when the user presses the right mouse button.
XUL has three different types of popups, described below. The main difference is the way in which they appear.
All three types of popups differ in the way that the user invokes them. They can contain any content, although menus are common for the plain and context popups and a simple string of text is common for a tooltip. The type of popup is determined by the element that invokes the popup.
A popup is described using the popup element. It has no special attributes and is a type of box. When invoked, it will display a window containing whatever you put inside the popup. However, you should always put an id attribute on the popup as it used to associate the popup with an element. We'll see what this means soon. First, an example:
<popupset>
<popup id="clipmenu">
<menuitem label="Cut"/>
<menuitem label="Copy"/>
<menuitem label="Paste"/>
</popup>
</popupset>As can be seen here, a simple popup menu with three commands on it has been created. The popup element surrounds the three menu items. It is much like the menupopup element. It is a type of box and defaults to vertical orientation. You will also notice that the id has been set on the popup element itself.
The popupset element surrounds the entire popup menu declaration. This is a generic container for popups, and is optional. It does not draw on screen but instead is used as a placeholder where you would declare all of your popups. As the name popupset implies, you can put multiple popup declarations inside it. Juts add additional ones after the first popup element. You can have more than one popupset in a file, but usually you will have only one.
Now that we've created the popup, it's time to make the popup appear. To do this we need to associate the popup with an element where it should appear. We do this because we only want the popup to appear when the user clicks in a certain area of a window. Typically, this will be a specific button or a box.
To associate the popup with an element, you add one of three attributes to the element. The attribute you add depends on which type of popup you want to create. For plain popups, add a popup attribute to the element. For context popups, add a context attribute. Finally, for tooltip popups, add a tooltip attribute.
The value of the attribute must be set to the id of the popup that you want to have appear. This is why you must put the id on the popup. That way it's easy to have multiple popups in a file.
In the example above, we want to make the popup a context menu. That means that we need to use the context attribute and add it to the element which we want to have the popup associated with. The sample below shows how we might do this:
Example 5.3.1: Source View<popupset>
<popup id="clipmenu">
<menuitem label="Cut"/>
<menuitem label="Copy"/>
<menuitem label="Paste"/>
</popup>
</popupset>
<box context="clipmenu">
<description value="Context click for menu"/>
</box>
Here, the popup has been associated with a box. Whenever you context-click
(right-click) anywhere inside the box, the popup menu will appear. The
popup will also appear even if you click on the children of the box so it
will work if you click on the description element also. The
context attribute has been used to associate the
box with a popup with the same id. In this case,
the popup clipmenu will appear. This way, you can
have a number of popups and associate them with different elements.
You could associate multiple popups with the same element by putting more attributes of different types on an element. You could also associate the same popup with multiple elements which is one advantage of using the popup syntax. Popups can only be associated with XUL elements. They cannot be associated with HTML elements.
We'll look at a simple way to create tooltips here. There are two ways to create a tooltip. The simplest way, which is much more common, is to add a tooltiptext attribute to an element for which you want to assign a tooltip.
The second method is to use a tooltip element containing the content of a tooltip. This requires you to have a separate block of content for each tooltip or have a script containing the content, however it does allow you to use any content besides text in a tooltip.
Example 5.3.2: Source View<button label="Save" tooltiptext="Click here to save your stuff"/> <tooltip id="moretip" orient="vertical" style="background-color: #33DD00;"> <description value="Click here to see more information"/> <description value="Really!" style="color: red;"/> </tooltip> <button label="More" tooltip="moretip"/>
These two buttons each have a tooltip. The first uses the default tooltip style. The second uses a custom tooltip that has a different background color and styled text. The tooltip is associated with the More button using the tooltip attribute, which is set to the corresponding id of the tooltip element.
By default, the popup and context windows will appear where the mouse pointer is. Tooltips will be placed slightly below the element so that the mouse pointer does not obscure it. There are cases however, where you will want to indicate in more detail where the popup appears. For example, the popup menu that appears when you click the Back button in a browser should appear underneath the back button, not where the mouse pointer is.
To change the popup position, you can use an additional attribute, position, on the popup. You can also add it to the menupopup element. This attribute is used to indicate the placement of the popup relative to the element invoking the popup. It can be set to a number of values, which are described briefly below:
By adding one or both of these attributes to an element, you can specify precisely where the popup appears. You cannot specify an exact pixel position. The position attribute can be used with all three popup types, although you probably wouldn't change the value for tooltips.
The example below demonstrates creating a back button with a popup menu:
Example 5.3.3: Source View<popupset>
<popup id="backpopup" position="after_start">
<menuitem label="Page 1"/>
<menuitem label="Page 2"/>
</popup>
</popupset>
<button label="Pop Me Up" popup="backpopup"/>Let's add a simple popup menu to the find files dialog. For simplicity, we'll just replicate the contents of the Edit menu. Let's have the popup appear when clicking over the first tab panel:
<popupset> <popup id="editpopup"> <menuitem label="Cut" accesskey="t"/> <menuitem label="Copy" accesskey="c"/> <menuitem label="Paste" accesskey="p" disabled="true"/> </popup> </popupset> <vbox flex="1"> . . . <tabpanel id="searchpanel" orient="vertical" context="editpopup">
Here a simple popup that is similar to the edit menu has been added to the first tabpanel. If you right-click (Control-click on the Macintosh) anywhere on the first panel, the popup will appear. However, the popup will not appear if you click anywhere else. Note that the textbox has its own built-in popup menu which will override the one we specified.
This section will describe scrolling menus and how to use the mechanism with other elements.
You might wonder what happens if you create a menu with a lot of commands on it, such that all the items won't fit on the screen at once. Mozilla will provide a scrolling mechanism that will allow you to scroll through the items.
If the available space is too small, arrows will appear on each end of the menu.
If you move the mouse over the arrows, the menu will scroll up and down. If the
available space is large enough, the arrows will not appear. Note that the exact
behavior of the scrolling will depend on the current theme.
This behavior is automatic. You do not have to do anything in order to get scrolling menus. It will apply to menus on menubars, in popups or menulists. It is implemented using an arrowscrollbox element. This element can be used to create a scrolling box with arrows.
The arrowscrollbox can be used anywhere a regular box can be used. You don't have to use it in menus. It is always a vertical box and may contain any elements inside it. You could use it to implement a list when you don't want it to be a drop-down.
The following example shows how to create a scrolling list of buttons (you will need to resize the window to see the arrow buttons):
Example 5.4.1: Source View<arrowscrollbox orient="vertical" flex="1"> <button label="Red"/> <button label="Blue"/> <button label="Green"/> <button label="Yellow"/> <button label="Orange"/> <button label="Silver"/> <button label="Lavender"/> <button label="Gold"/> <button label="Turquoise"/> <button label="Peach"/> <button label="Maroon"/> <button label="Black"/> </arrowscrollbox>
If you try this example, it will first open at full size. However, if you shrink the height of the window, the scroll arrows will appear. Making the window larger again will cause the arrows to disappear.
You can set a CSS max-height property on the arrowscrollbox to limit the size of the scrolling box and thus make the arrows appear all the time.
The arrowscrollbox is mainly useful in menus and popups however.
XUL has a set of elements for creating tabular grids.
HTML pages typically use tables for layout or for displaying a grid of data. XUL has a set of elements for doing this type of thing. The grid element is used to declare a grid of data. It has some similarities to the HTML table tag.
A grid contains elements that are aligned in rows just like tables. Inside a grid, you declare two things, the columns that are used and the rows that are used. Just like HTML tables, you put content such as labels and buttons inside the rows. You do not put content inside the columns but you can use them to specify the size and appearance of the columns in a grid. Alternatively, you can put content inside the columns, and use the rows to specify the appearance. We'll look at the case of organizing elements by row first.
To declare a set of rows, use the rows tag, which should be a child element of grid. Inside that you should add row elements, which are used for each row. Inside the row element, you should place the content that you want inside that row.
Similarly, the columns are declared with the columns element, which should be placed as a child element of the grid. Inside that go individual column elements, one for each column you want in the grid.
This should be easier to understand with an example.
Example 6.1.1: Source View<grid flex="1">
<columns>
<column flex="2"/>
<column flex="1"/>
</columns>
<rows>
<row>
<button label="Rabbit"/>
<button label="Elephant"/>
</row>
<row>
<button label="Koala"/>
<button label="Gorilla"/>
</row>
</rows>
</grid>
Two rows and two columns have been added to a grid. Each column is declared
with the column tag. Each column has been given a
flex attribute. Each row contains two elements,
both buttons. The first element in each row element
is placed in the first column of the grid and the second element is each row is
placed in the second column. Note that you do not need an element to
declare a cell. (there is no equivalent of the HTML
td element). Instead, you put the contents of cells
directly in the row elements.
You can use any element besides a button element of course. If you wanted one particular cell to contain multiple elements, you can use a nested box. A box is a single element but you can put as many elements that you want inside it. For example:
Example 6.1.2: Source View<grid flex="1">
<columns>
<column/>
<column flex="1"/>
</columns>
<rows>
<row>
<label control="doctitle" value="Document Title:"/>
<textbox id="doctitle" flex="1"/>
</row>
<row>
<label control="docpath" value="Path:"/>
<box flex="1">
<textbox id="docpath" flex="1"/>
<button label="Browse..."/>
</box>
</row>
</rows>
</grid>
Notice in the image how the first column of elements containg the labels
has only a single element in each row. The second column contains a box
in its second row, which in turn contains two elements, a
textbox and a button.
You could add additional nested boxes or even another grid inside inside
a single cell.
If you resize the window of the last example, you will see that the textboxes resize, but no other elements do. This is because of the flex attributes added to the textboxes and the second column. The first column does not need to be flexible as the labels do not need to change size.
The initial width of a column is determined by the largest element in the column. Similarly, the height of a row is determined by the size of the elements in a row. You can use the CSS properties min-width and max-width and related properties to further define the size.
You can also place the elements inside the column elements instead of the rows. If you do this, the rows are just declared to specify how many rows there are.
Example 6.1.3: Source View<grid>
<rows>
<row/>
<row/>
<row/>
</rows>
<columns>
<column>
<label control="first" value="First Name:"/>
<label control="middle" value="Middle Name:"/>
<label control="last" value="Last Name:"/>
</column>
<column>
<textbox id="first"/>
<textbox id="middle"/>
<textbox id="last"/>
</column>
</columns>
</grid>This grid has three rows and two columns. The row elements are just placeholders to specify how many there are. You may add the flex attribute to a row to make it flexible. The content is placed inside in each column. The first element inside each column element is placed in the first row, the second element in the second row and the third element is placed in the third row.
If you put content in both the columns and the rows, the content will overlap each other, although they will align in a grid properly. It creates an effect much like a grid of stack elements.
The order of the elements in the grid determines which is displayed on top and which is placed underneath. If the rows element is placed after the columns element, the content within the rows is displayed on top. If the columns element is placed after the rows element, the content within the columns is displayed on top. Similarly, events such as mouse buttons and keypresses are sent only to the set on top. This is why the columns were declared after the rows in the above example. If the columns has been placed first, the rows would have grabbed the events and the fields could not be typed into.
One primary advantage that grids have over a set of nested boxes is that you can create cells that are flexible both horizontally and vertically. You can do this by putting a flex attribute on both a row and a column. The following example demonstrates this:
Example 6.1.4: Source View<grid flex="1">
<columns>
<column flex="5"/>
<column/>
<column/>
</columns>
<rows>
<row flex="10">
<button label="Cherry"/>
<button label="Lemon"/>
<button label="Grape"/>
</row>
<row flex="1">
<button label="Strawberry"/>
<button label="Raspberry"/>
<button label="Peach"/>
</row>
</rows>
</grid>The first column and both rows have been made flexible. This will result in every cell in the first column being flexible horizontally. In addition, every cell will be flexible vertically because both rows are flexible, although the first row is more so. The cell in the first column and first row (the Cherry button) will be flexible by a factor of 5 horizontally and flexible by a factor of 10 vertically. The next cell, (Lemon) will only be flexible vertically.
The flex attribute has also been added to the grid element so that the entire grid is flexible, otherwise it would only grow in one direction.
The listbox element was described in an earlier section. It has a number of additional features that are described below.
The listbox element is used to create a list of items from which the user can select one or more items from. It is internally created in Mozilla using a grid. It simply uses a set of custom tags and adds code to handle selection automatically. You might use it when you need a grid with selectable rows.
Because it is a grid, it supports multiple columns. Each cell may have arbitrary content within it, although usually only text is used. In a listbox, the content always goes in the rows, unlike a grid where the content may be placed either in the rows or the columns. The syntax is similar but different tags are used. The following table lists the equivalent tags in a listbox:
| Description | Tag in a Grid | Tag in a Listbox |
| Outer element | grid | listbox |
| A group of columns | columns | listcols |
| A single column | column | listcol |
| A group of rows | rows | ( no equivalent ) |
| A single row | row | listitem |
| A single cell | ( no equivalent ) | listcell |
List boxes have no equivalent of the grid's rows element. Actually, there is an equivalent, but it is created automatically and placed in the list box for you. It is also hidden so you don't need to even know it is there.
The listcell element may be used for each cell in the grid. To specify the text content of a cell, place a label attribute on a listcell. For the simple case where there is only one column, you may also place the label attributes directly on the listitem elements and leave the listcell elements out entirely.
When the user selects an item, the entire row is selected. You cannot have a single cell selected.
The following example is of a listbox with two columns and three rows:
Example 6.2.1: Source View<listbox>
<listcols>
<listcol flex="1"/>
<listcol flex="2"/>
</listcols>
<listitem>
<listcell label="George"/>
<listcell label="House Painter"/>
</listitem>
<listitem>
<listcell label="Mary Ellen"/>
<listcell label="Candle Maker"/>
</listitem>
<listitem>
<listcell label="Roger"/>
<listcell label="Swashbuckler"/>
</listitem>
</listbox>This list box has two columns, the second one being twice as flexible as the first one. If you shrink the size horizontally, the labels on the cells will crop themselves automatically. As with other elements, you can use the crop attribute on the cells or items to control the cropping.
List boxes also allow a special header row to be used. This is much like a regular row except that it is displayed differently. You would use this to create column headers. Two new elements are used.
The listhead element is used for the header rows, just
as the listitem element is used for regular rows. The
header row is not a normal row however, so using a script to get the first row
in the list box will skip the header row.
The listheader element is used for each cell in the header row. Use the label to set the label for the header cell.
Here is the earlier example with a header row:
Example 6.2.2: Source View<listbox>
<listhead>
<listheader label="Name"/>
<listheader label="Occupation"/>
</listhead>
<listcols>
<listcol flex="1"/>
<listcol flex="2"/>
</listcols>
<listitem>
<listcell label="George"/>
<listcell label="House Painter"/>
</listitem>
<listitem>
<listcell label="Mary Ellen"/>
<listcell label="Candle Maker"/>
</listitem>
<listitem>
<listcell label="Roger"/>
<listcell label="Swashbuckler"/>
</listitem>
</listbox>XUL provides a way to create tabular or hierarchical lists using a tree.
One of the more complex elements in XUL is the tree. Like a listbox. a tree may be used for creating lists of items. The tree element may also used for creating hierarchical lists and tables. For example, the list of messages in a mail application, or the Edit Bookmarks window in Mozilla can be created using a tree.
The tree has many of the same features as the listbox. Both can be used to create tables of data with multiple rows and columns, and both may contain column headers. The tree also supports nested rows, whereas the listbox does not. The listbox is intended to be a lightweight alternative to the tree, for simpler cases, so use it instead when you can.
There are several ways that the contents of a tree can be created. The simplest is by putting tree items directly within the tree element. We'll look at this method in this section. The other method, which is actually more common and useful allows you to create the tree contents from a data source. This means that the tree can be filled with information from the user's bookmark list, from a directory on disk, or from the results of a search, or a number of other places. Trees may also be populated with a script. We'll see these latter two methods in later sections.
Trees can be created with the tree element, which is decribed in the following sections. Related elements are used to specify columns and rows. Much like menus, each element is nested inside each other. The elements that make up a tree are described briefly below:
Let's start by looking at how to create a simple table with multiple columns. This could be used to create a list of mail messages. There might be multiple columns, such as sender, subject and date.
First, the entire table is surrounded with a tree element. This declares an element that is used as a table or tree. In this case, we'll use it like a table.
There are two parts to a tree's syntax. The first part is used to declare the content of the tree. Like HTML tables, the data is organized into rows. The table data is created by placing a single row or rows of data inside a treechildren element. The second part is used to specify what columns you want in a tree, using the treecols tag. This is much like how listboxes and the grids are created.
You may place as many columns as you wish in a tree. Like with listboxes, a header row will appear with column labels. A drop-down menu will appear in the upper-right corner of the tree, which the user may use to show and hide individual columns.
Each column is created with a treecol element. You can set the header label using the label attribute. You may also want to make the columns flexible if your tree is flexible, so that the columns stretch as the tree does. All of the columns should be placed directly inside a treecols element.
The body of the tree is created using four elements. The treechildren element surrounds all of the rows. Inside the body are individual rows, which may in turn contain other rows. Trees with nested rows are described in the next section. For a simpler tree, each row is created with the treeitem and treerow elements. The former is used for a row and its child rows and the latter is used for a single row.
Inside the rows, you will put individual tree cells. These are created using the treecell element. You can set the text for the cell using the label attribute. The first treecell in a row determines the content that will appear in the first column, the second treecell determines the content that will appear in the second column, and so on.
The sample below shows a tree created with the syntax so far.
Example 6.3.1: Source View<tree flex="1">
<treecols>
<treecol id="sender" label="Sender" flex="1"/>
<treecol id="subject" label="Subject" flex="2"/>
</treecols>
<treechildren>
<treeitem>
<treerow>
<treecell label="joe@somewhere.com"/>
<treecell label="Top secret plans"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="mel@whereever.com"/>
<treecell label="Let's do lunch"/>
</treerow>
</treeitem>
</treechildren>
</tree>
As can be seen in the image, the tree has been created with two rows of data.
This tree has two columns, the second of which will take up more space than the first. You will usually make the columns flexible You can also supply widths with the CSS width property. You should include the same number of treecol elements as there are columns in the tree. Otherwise strange things will happen.
The header row is created automatically. The button in the upper right corner can be used to hide and show the columns. You can place a hidecolumnpicker attribute on the tree and set it to true if you would like to hide this button. Make sure that you set id attribute on each column or the hiding and showing of columns will not work.
The user can select the treeitems by clicking on them with the mouse, or by highlighting them with the keyboard. The user can select multiple items by holding down the Shift or Control keys and clicking additional rows. To disable multiple selection, place a seltype attribute on the tree, set to the value single. With this, the user may only select a single row at a time.
Let's add a tree to the find files window where the results of the search would be displayed. The following code should be added in place of the iframe.
<tree flex="1">
<treecols>
<treecol id="name" label="Filename" flex="1"/>
<treecol id="location" label="Location" flex="2"/>
<treecol id="size" label="Size" flex="1"/>
</treecols>
<treechildren>
<treeitem>
<treerow>
<treecell label="mozilla"/>
<treecell label="/usr/local"/>
<treecell label="2520 bytes"/>
</treerow>
</treeitem>
</treechildren>
</tree>
<splitter collapse="before" resizeafter="grow"/>We've added a tree with three columns for the filename, the location and the file size. The second column will appear twice as wide due to the larger flexibility. A single row has been added to demonstrate what the table would look like with a row. In a real implementation, the rows would be added by a script as the search was performed.
Here, we'll see more features of trees.
The tree element is also used to create hierarchical lists, like that found in a file manager or a browser's bookmarks list. To do this, you need to do two things. First, you need to mark the treeitem element that has children as a container. You do this by adding the container attribute to it as follows:
<treeitem container="true"/>
This allows the user to double-click on the treeitem to expand and collapse the inner rows. You can have the child rows initially displayed by adding the open attribute. When the user expands and collapses the parent, this attribute is changed to reflect the current state.
The second change is that you must put the primary attribute on the first column. This causes a small triangle or plus sign to appear before the cells in that column to indicate the cells can be expanded. In addition, child rows are indented. Note also that the user cannot hide the primary column.
The children themselves can be specified by placing a treechildren element inside the treeitem. Don't put it inside the treerow as this won't work.
You can repeat this process to create deeply nested trees. Essentially, a treeitem element can contain either single rows which are declared with the treerow element or a set of rows which are declared with the treechildren element.
The following is a simple example:
Example 6.4.1: Source View<tree flex="1">
<treecols>
<treecol id="firstname" label="First Name" primary="true" flex="3"/>
<treecol id="lastname" label="Last Name" flex="7"/>
</treecols>
<treechildren>
<treeitem container="true" open="true">
<treerow>
<treecell label="Guys"/>
</treerow>
<treechildren>
<treeitem>
<treerow>
<treecell label="Bob"/>
<treecell label="Carpenter"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="Jerry"/>
<treecell label="Hodge"/>
</treerow>
</treeitem>
</treechildren>
</treeitem>
</treechildren>
</tree>
This has created a hierarchical tree. As can be seen in the image, a small
plus or minus sign (often called a twisty) has appeared next to the first row,
indicating that it contains child rows. By double-clicking the row, the user
can open and close the list. The child rows are indented. Notice how the
Guys row only needs one column as it is a placeholder item for its children.
The outer treeitem element contains a single treerow element and a treechildren element. The former creates the data for the parent row and the latter contains the child items.
You can nest rows deeper as well. Remember that you must use the container attribute on rows which contain child rows. The simple presence of child rows isn't sufficient to indicate this, as you may have a container that has no children but should still be treated like a container. For example, a directory with no files in it should still be treated like a container whereas a file should not.
One additional attribute you can add to the tree is enableColumnDrag. (Note the mixed case.) If enabled, the user may drag the column headers around to rearrange the order of the columns.
A user will also likely want to change the column widths. You can do this by placing a splitter element in between each treecol element. A small notch will appear in between each column header which the user may drag to change the width of a column. You can set the max-width CSS property of the splitter to 0 to hide the notch, although the column may still be resized. The global stylesheet provides a class tree-splitter which does this for you.
You can set a minimum or maximum width of a column using the min-width or max-width style properties on the column header.
You can set the hidden attribute on a column to true to have the column hidden by default. The user can choose to show the column by selecting it from the drop-down on the end of the header row.
As with all elements, the persist attribute can be used to save the state of the columns in-between sessions. Thus, once the user has decided on a column layout they like, it will automatically be saved for next time. You will need to save a number of attributes as indicated in the example below:
Example 6.4.2: Source View<tree enableColumnDrag="true" flex="1">
<treecols>
<treecol id="runner" label="Runner" flex="2" persist="width ordinal hidden"/>
<splitter class="tree-splitter"/>
<treecol id="city" label="Home City" flex="2" persist="width ordinal hidden"/>
<splitter class="tree-splitter"/>
<treecol id="starttime" label="Start Time" flex="1" persist="width ordinal hidden"/>
<splitter class="tree-splitter"/>
<treecol id="endtime" label="End Time" flex="1" persist="width ordinal hidden"/>
</treecols>
<treechildren>
<treeitem>
<treerow>
<treecell label="Joshua Granville"/>
<treecell label="Vancouver"/>
<treecell label="7:06:00"/>
<treecell label="9:10:26"/>
</treerow>
</treeitem>
<treeitem>
<treerow>
<treecell label="Robert Valhalla"/>
<treecell label="Seattle"/>
<treecell label="7:08:00"/>
<treecell label="9:15:51"/>
</treerow>
</treeitem>
</treechildren>
</tree>Three attributes of the columns must be persisted, the width attribute to save the column widths, the ordinal attribute which holds the position of the column, and the hidden attribute which holds whether the column is hidden or visible.
In this section, we'll look at RDF (Resource Description Framework).
We can use the tree elements to display a set of data, such as bookmarks or mail messages. However, it would be inconvenient to do so by entering the data directly into the XUL file. It would make it very difficult to modify the bookmarks if they were directly in the XUL file. The way to solve this is by using RDF datasources.
RDF (Resource Description Framework) is a file format that can be used to store resources such as bookmarks or mail. Alternatively, data in other formats can be used and code written that will read the file and create RDF data from it. This is how Mozilla works when it reads data such as bookmarks, the history or mail messages. Mozilla provides datasources for this common data among others so that you can easily use them.
You can use any of the provided RDF datasources to populate trees with data or you can point to an RDF file that contains the data. This makes it very convenient to display trees with lots of rows in them. RDF can also populate other XUL elements as well such as listboxes and menus. We'll see this in the next section.
Here, a brief overview of RDF will be provided. A good way to understand it is to look at some of the RDF files provided with Mozilla. They have the extension rdf.
RDF, like XUL, is an XML-based language. It contains a fairly simple set of elements. The sample below shows a simple RDF template
<?xml version="1.0"?> <RDF:RDF xmlns:RDF="http://www.w3.org//1999/02/22-rdf-syntax-ns#"> ... </RDF:RDF>
This has some similarities to the XUL header. Instead of the window element, the RDF element is used. You can see the namespace for RDF was declared so that the RDF elements are recognized properly. Inside, the RDF element, you will place the data.
A brief description of RDF will be given here. For more information about RDF, see the RDF specification. Let's take the example of a bookmarks list generated from RDF. A bookmarks list contains a set of records, each with a set of data associated with it, such as a URL, a bookmark title and a visited date.
Think of the bookmarks as a database, which is stored as a large table with numerous fields. In the case of RDF however, the lists may be hierarchical as well. This is necessary so that we can have folders or categories of bookmarks. Each of the fields in an RDF database is a resource, each with a name associated with it. The name is described by a URI.
For example, a selection of the fields in the Mozilla bookmark list are described by the URIs below:
| Name | http://home.netscape.com/NC-rdf#Name | Bookmark name |
| URL | http://home.netscape.com/NC-rdf#URL | URL to link to |
| Description | http://home.netscape.com/NC-rdf#Description | Bookmark description |
| Last Visited | http://home.netscape.com/WEB-rdf#LastVisitDate | Date of last visit |
These are generated by taking a namespace name and appending the field name. In the next section, we'll look at how we can use these to fill in the field values automatically. Note that the last modified date has a slightly different namespace than the other three.
Below, a sample RDF file is shown, listing a table with three records and three fields.
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:ANIMALS="http://www.some-ficticious-zoo.com/rdf#">
<RDF:Seq about="urn:animals:data">
<RDF:li>
<RDF:Description about="urn:animals:lion">
<ANIMALS:name>Lion</ANIMALS:name>
<ANIMALS:species>Panthera leo</ANIMALS:species>
<ANIMALS:class>Mammal</ANIMALS:class>
</RDF:Description>
</RDF:li>
<RDF:li>
<RDF:Description about="urn:animals:tarantula">
<ANIMALS:name>Tarantula</ANIMALS:name>
<ANIMALS:species>Avicularia avicularia</ANIMALS:species>
<ANIMALS:class>Arachnid</ANIMALS:class>
</RDF:Description>
</RDF:li>
<RDF:li>
<RDF:Description about="urn:animals:hippopotamus">
<ANIMALS:name>Hippopotamus</ANIMALS:name>
<ANIMALS:species>Hippopotamus amphibius</ANIMALS:species>
<ANIMALS:class>Mammal</ANIMALS:class>
</RDF:Description>
</RDF:li>
</RDF:Seq>
</RDF:RDF>Here, three records have been described, one for each animal. Each RDF:Description tag describes a single record. Within each record, three fields are described, name, species and type. It isn't necessary for all records to have the same fields but it makes more sense to have them all the same.
Each of three fields have been given a namespace of ANIMALS, the URL of which has been declared on the RDF tag. The name was selected because it has meaning in this case, but we could have selected something else. The namespace feature is useful because the class field might conflict with that used for styles.
The Seq and li elements are used to specify that the records are in a list. This is much like how HTML lists are declared. The Seq element is used to indicate that the elements are ordered, or in sequence. Instead of the Seq element, you can also use Bag to indicate unordered data, and Alt to indicate data where each record specifies alternative values (such as mirror URLs).
The resources can be referred to in a XUL file by combining the namespace URL followed by the field name. In the example above, the following URIs are generated which can be used to refer to the specific fields:
| Name | http://www.some-ficticious-zoo.com/rdf#name |
| Species | http://www.some-ficticious-zoo.com/rdf#species |
| Class | http://www.some-ficticious-zoo.com/rdf#class |
In this section, we'll find out to populate elements with data.
XUL provides a method in which we create elements from data supplied by RDF, either from an RDF file or from an internal datasource. Numerous datasources are provided with Mozilla such as bookmarks, history and mail messages. More details on these will be provided in the next section.
Usually, elements such as treeitems and menuitems will be populated with data. However, you can use other elements if you want although they are more useful for specialized cases. Nevertheless, we'll start with these other elements because trees and menus require more code.
To allow the creation of elements based on RDF data, you need to provide a simple template which will be duplicated for each element that is created. Essentially, you provide only the first element and the remaining elements are constructed based on the first one.
The template is created using the template element. Inside it, you can place the elements that you want to use for each constructed element. The template element should be placed inside the container that will contain the constructed elements. For example, if you are using a tree, you should place the template element inside a tree element.
This is better explained with an example. Let's take a simple example where we want to create a button for each top-level bookmark. Mozilla provides a bookmarks datasource so it can be used to get the data. This example will only get the top-level bookmarks (or bookmark folders) as we're going to create buttons. For child bookmarks, we would need to use an element that displays a hierarchy such as a tree or menu.
To view this example, you will need to create a chrome package and load the file from there. You can then enter the chrome URL into the browser URL field.
Example 6.6.1: Source<vbox datasources="rdf:bookmarks" ref="NC:BookmarksRoot" flex="1">
<template>
<button uri="rdf:*" label="rdf:http://home.netscape.com/NC-rdf#Name"/>
</template>
</vbox>
Here, a vertical box has been created that will contain a column of buttons, one
for each top-level bookmark. You can see that the template
contains a single button. This single button is used
as a basis for all the buttons that need to be created. You can see in the image
that the set of buttons has been created, one for each bookmark.
Try adding a bookmark in the browser while you have one of the example window open. You'll notice that the buttons in the example get updated instantly. (You may have to focus the window for it to change.)
You might also try adding or modifying your bookmarks open and noting that the buttons are updated automatically.
The template itself is placed inside a vertical box. The box has two special attributes that allow it to be used for templates, which are used to specify where the data comes from. The first attribute on the box is the datasources attribute. This is used to declare what RDF datasource will be providing the data to create the elements. In this case, rdf:bookmarks is used. You can probably guess that this means to use the bookmarks datasource. This datasource is provided by Mozilla. To use your own datasource, specify the URL of an RDF file for the datasources attribute, as indicated in the example below:
<box datasources="chrome://zoo/content/animals.rdf"
ref="urn:animals:data">You can even specify multiple datasources at a time by separating them with a space in the attribute value. This can be used to display data from multiple sources.
The ref attribute indicates where in the datasource you would like to retrieve data from. In the case of the bookmarks, the value NC:BookmarksRoot is used to indicate the root of the bookmarks hierarchy. Other values that you can use will depend on the datasource you are using. If you are using your own RDF file as a datasource, the value will correspond to the value of an about attribute on an RDF Bag, Seq or Alt element.
By adding these two attributes to the box above, it allows the generation of elements using the template. However, the elements inside the template need to be declared differently. You may notice in the example above that the button has a uri attribute and an unusual value for the label attribute.
An attribute value inside the template that begins with 'rdf:' indicates that the value should be taken from the datasource. In the example earlier, this is the case for the label attribute. The remainder of the value refers to the name property is the datasource. It is constructed by taking the namespace URL used by the datasource and appending the property name. If you don't understand this, try re-reading the last part of the previous section. It explains how resources in RDF can be referred to. Here, we only use the name of the bookmark but numerous other fields are available.
The label of the buttons is set to this special URI because we want the labels on the buttons to be set to the names of the bookmarks. We could have put a URI in any of the attributes of the button, or any other element. The values of these attributes are replaced with data supplied by the datasource which, in this case, is the bookmarks. So we end up with the labels on the buttons set to the names of the bookmarks.
The example below shows how we might set other attributes of a button using a datasource. Of course, this assumes that the datasource supplies the appropriate resources. If a particular one is not found, the value of the attribute will be set to an empty string.
<button class="rdf:http://www.example.com/rdf#class"
uri="rdf:*"
label="rdf:http://www.example.com/rdf#name"/>
crop="rdf:http://www.example.com/rdf#crop"/>As you can see, you can dynamically generate lists of elements with the attributes provided by a separate datasource.
The uri attribute is used to specify the element where content generation will begin. Content earlier will only be generated once whereas content inside will be generated for each resource. We'll see more about this when we get to creating templates for trees.
By adding these features to the container the template is in, which in this case is a box, and to the elements inside the template, we can generate various interesting lists of content from external data. We can of course put more than one element inside a template and add the special RDF references to the attributes on any of the elements. The example below demonstrates this.
Example 6.6.2: Source<vbox datasources="rdf:bookmarks" ref="NC:BookmarksRoot" flex="1">
<template>
<vbox uri="rdf:*">
<button label="rdf:http://home.netscape.com/NC-rdf#Name"/>
<label value="rdf:http://home.netscape.com/NC-rdf#URL"/>
</vbox>
</template>
</vbox>This creates a vertical box with a button and a label for each bookmark. The button will have the name of the bookmark and the label will have the URL.
The new elements that are created are functionally no different from ones that you put directly in the XUL file. The id attribute is added to every element created through a template which is set to a value which identifies the resource. You can use this to identify the resource.
You can also specify mutliple resource values in the same attribute by separating them with a space, as in the example below. More about resource syntax.
Example 6.6.3: Source<vbox datasources="rdf:bookmarks" ref="NC:BookmarksRoot"
flex="1">
<template>
<label uri="rdf:*" value="rdf:http://home.netscape.com/NC-rdf#Name rdf:http://home.netscape.com/NC-rdf#URL"/>
</template>
</vbox>In the image of the earlier example, you may have noticed that the third button is simply a button with hyphens on it. This is a separator in the bookmark list. In the way that we have been using it, the RDF bookmarks datasource supplies the separators as if they were just regular bookmarks. What we would really like to do is add a small amount of spacing instead of a button for separator resources. That means that we want to have two different types of content be created, one type for regular bookmarks and a second type for separators.
We can do this by using the rule element. We define a rule for each variation of elements that we want to have created. In our case, we would need a rule for bookmarks and a rule for separators. Attributes placed on the rule element determine which rules apply to which RDF resource.
When scanning for which rule applies to the data, each rule element is checked in sequence for a match. That means that the order in which you define rules is important. Earlier rules will override later rules.
The following example demonstrates the earlier example with two rules:
Example 6.6.4: Source<window
id="example-window"
title="Bookmarks List"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<vbox datasources="rdf:bookmarks" ref="NC:BookmarksRoot" flex="1">
<template>
<rule rdf:type="http://home.netscape.com/NC-rdf#BookmarkSeparator">
<spacer uri="rdf:*" height="16"/>
</rule>
<rule>
<button uri="rdf:*" label="rdf:http://home.netscape.com/NC-rdf#Name"/>
</rule>
</template>
</vbox>
</window>
By using two rules, we have allowed the contents of the template to be selectively
generated. In the first rule, bookmark separators are
selected, as can be seen by the rdf:type attribute.
The second rule does not have any attributes so all data
matches it.
All of the attributes placed on the rule tag are used as match criteria. In this case, the bookmarks datasource supplies a rdf:type property to distinguish separators. This attribute is set to a special value for separators in the RDF bookmarks datasource. This is how we can distinguish them from non-separators. You can use a similar technique for any attribute that might be on an RDF Description element.
The special URL value given in the example above for the first rule is used for separators. That means that separators will follow rule one and generate a spacer element, which will display a 16 pixel gap. Elements that are not separators will not match rule one and will fall through to rule two. Rule two does not have any attributes on it. This means that it will match all data. This is, of course, what we want to have happen to the rest of the data.
You should also have noticed that because we wanted to get an attribute from the RDF namespace (rdf:type), we needed to add the namespace declaration to the window tag. If we didn't do this, the attribute would be looked for in the XUL namespace. Because it does not exist there, the rule will not match. If you use attributes in your own custom namespace, you need to add the namespace declaration in order to match them.
You should be able to guess what would happen if the second rule was removed. The result would be a single spacer displayed but no bookmarks because they don't match any of the rules.
Put simply, a rule matches if all of the attributes placed on the rule element match the corresponding attributes on the RDF resource. In the case of an RDF file, the resources would be the Description elements.
There are some small exceptions however. You cannot match based on the attributes id, rdf:property or rdf:instanceOf. Because you can just use your own attributes with your own namespace, it probably doesn't really matter anyway.
Note that a template with no rules in it, as in the first example, is really equivalent functionally to a template with a single rule with no attributes.
The following describes how to use a template with a tree.
When using a tree, you will often use a template to build its content, to handle a large amount of hierarchial data. Using a template with a tree uses very much the same syntax as with other elements. You need to add a datasources and a ref attribute to the tree element, which specify the datasource and root node to display. Multiple rules can be used to indicate different content for different types of data.
The following example uses the history datasource:
<tree datasources="rdf:history" ref="NC:HistoryByDate"
flags="dont-build-content">Normally, when a template builds content, it generates XUL elements for each node in the datasource. Because trees often have thousands of rows, this would be inefficient. The flags attribute set to the value dont-build-content, as used in the example above, instructs the template builder to not generate this content. You can see the difference by using Mozilla's DOM Inspector on a tree with and without the flag.
There will be one treecell for each column in the tree. The cells should have a label attribute to set the label for the cell. This would normally be set to an RDF property so that the label is pulled from the datasource.
The following example demonstrates a template-built tree, in this case for the file system.
Example 6.7.1: Source<tree id="my-tree" flex="1"
datasources="rdf:files" ref="file:///" flags="dont-build-content">
<treecols>
<treecol id="Name" label="Name" primary="true" flex="1"/>
<splitter/>
<treecol id="Date" label="Date" flex="1"/>
</treecols>
<template>
<rule>
<treechildren flex="1">
<treeitem uri="rdf:*">
<treerow>
<treecell label="rdf:http://home.netscape.com/NC-rdf#Name"/>
<treecell label="rdf:http://home.netscape.com/WEB-rdf#LastModifiedDate"/>
</treerow>
</treeitem>
</treechildren>
</rule>
</template>
</tree>Here, a tree is created with two columns, for the name and date of a file. The tree should display a list of the files in the root directory. Only one rule is used, but you may add others if needed. Like with other templates, the uri attribute on an element indicates where to start generating content. The two cells grab the name and date from the datasource and place the values in the cell labels.
This example shows why the uri attribute becomes useful. Notice how it has been placed on the treeitem in the example, even though it is not a direct descendant of the rule element. We need to put this attribute on only those elements that we want repeated for each resource. Because we don't want multiple treechildren elements, we don't put it there. Instead we put the uri attributes on the treeitem elements. Effectively, the elements outside (or above) the element with the uri attribute are not duplicated whereas the element with the uri attribute and the elements inside it are duplicated for each resource.
Note in the image that additional child elements below the top-level elements have been added automatically. XUL knows how to add child elements when the templates or rules contain tree elements or menu elements. It will generate tree elements as nested as the RDF data contains.
An interesting part of RDF datasources is that the resource values are only determined when the data is needed. This means that values that are deeper in the resource hierarchy are not determined until the user navigates to that node in the tree. This becomes useful for certain datasources where the data is determined dynamically.
If you try the previous example, you might note that the list of files is not sorted. Trees which generate their data from a datasource have the optional ability to sort their data. You can sort either ascending or descending on any column. The user may change the sort column and direction by clicking the column headers. This sorting feature is not available for trees with static content, although you can write a script to sort the data.
Sorting involves three attributes, which should be placed on the columns. The first attribute, sort, should be set to an RDF property that is used as the sort key. Usually, this would be the same as that used in the label of the cell in that column. If you set this on a column, the data will be sorted in that column. The user can change the sort direction by clicking the column header. If you do not set the sort attribute on a column, the data cannot be sorted by that column.
The sortDirection attribute (note the mixed case) is used to set the direction in which the column will be sorted by default. Three values are possible:
The final attribute, sortActive should be set to true for one column, the one that you would like to be sorted by default.
Although the sorting will function correctly with only those attributes, you may also use the style class sortDirectionIndicator on a column that can be sorted. This will cause a small triangle to appear on the column header that indicates the direction of the sort. If you don't do this, the user may still sort the columns but will have no indication as to which column is currently sorted.
The following example changes the columns in the earlier example to incorporate the extra features:
<treecols>
<treecol id="Name" label="Name" flex="1" primary="true"
class="sortDirectionIndicator" sortActive="true"
sortDirection="ascending"
sort="rdf:http://home.netscape.com/NC-rdf#Name"/>
<splitter/>
<treecol id="Date" label="Date" flex="1" class="sortDirectionIndicator"
sort="rdf:http://home.netscape.com/WEB-rdf#LastModifiedDate"/>
</treecols>One additional thing you might want to do is persist which column is currently sorted, so that it is remembered between sessions. To do this, we use the persist attribute on each treecol element. There are five attributes of columns that need to be persisted, to save the column width, the column order, whether the column is visible, which column is currently sorted and the sort direction. The following example shows a sample column:
<treecol id="Date" label="Date" flex="1"
class="sortDirectionIndicator"
persist="width ordinal hidden sortActive sortDirection"
sort="rdf:http://home.netscape.com/WEB-rdf#LastModifiedDate"/>There are two additional attributes that can be added to the rule element that allow it to match in certain special circumstances. Both are boolean attributes.
The two attributes above are really the reverse of each other. A resource might be a container and be an empty one as well. However, this is different from a resource that is not a container. For example, a bookmark folder is a container but it might or might not have children. However a single bookmark or separator is not a container.
You can combine these two elements with other attribute matches for more specific rules.
Here, we'll look at additional datasources and how to use your own RDF files as datasources.
Mozilla provides a number of other built-in datasources. Some of them are listed here with a few examples. They work very similarly to the bookmarks, although the fields will be different in each case.
The history datasource provides access to the user's history list which is the list of URLs the user has visited recently. The resource can be referred to using rdf:history as the datasource. The table below shows the resources (or fields) that you can retrieve from the history datasource. Put the URL values below where you want the value of the resource to be used.
| Date | rdf:http://home.netscape.com/NC-rdf#Date | Date of last visit |
| Name | rdf:http://home.netscape.com/NC-rdf#Name | Title of the page |
| Page | rdf:http://home.netscape.com/NC-rdf#Page | Page name |
| Referrer | rdf:http://home.netscape.com/NC-rdf#Referrer | Referrer of the page |
| URL | rdf:http://home.netscape.com/NC-rdf#URL | URL of the page |
| Visit Count | rdf:http://home.netscape.com/NC-rdf#VisitCount | Number of page visits |
A typical history list will display a tree with a selection of these fields. To use them, just put the URL values above in the label attributes of the buttons or treecells. You can use NC:HistoryRoot as the value of the ref attribute. You can also use the value NC:HistoryByDate to get the history sorted into days.
Let's see an example of displaying the history list. We'll display the history in a tree with three columns, the Name, the Page and the Date.
Example 6.8.1: Source<tree flex="1" datasources="rdf:history" ref="NC:HistoryRoot">
<treecols>
<treecol id="name" label="Name" flex="1"/>
<treecol id="url" label="URL" flex="1"/>
<treecol id="date" label="Date" flex="1"/>
</treecols>
<template>
<rule>
<treechildren flex="1">
<treeitem uri="rdf:*">
<treerow>
<treecell label="rdf:http://home.netscape.com/NC-rdf#Name"/>
<treecell label="rdf:http://home.netscape.com/NC-rdf#URL"/>
<treecell label="rdf:http://home.netscape.com/NC-rdf#Date"/>
</treerow>
</treeitem>
</treechildren>
</rule>
</template>
</tree>The tables below list some of the other datasources available with Mozilla. You can use any of the resources that you want.
Bookmarks (rdf:bookmarks): The bookmarks are generated from the user's bookmark list.
| Resources | ||
| Added Date | rdf:http://home.netscape.com/NC-rdf#BookmarkAddDate | Date the bookmark was added |
| Description | rdf:http://home.netscape.com/NC-rdf#Description | Bookmark description |
| Last Modified | rdf:http://home.netscape.com/WEB-rdf#LastModifiedDate | Date of last modification |
| Last Visited | rdf:http://home.netscape.com/WEB-rdf#LastVisitDate | Date of last visit |
| Name | rdf:http://home.netscape.com/NC-rdf#Name | Bookmark name |
| Shortcut URL | rdf:http://home.netscape.com/NC-rdf#ShortcutURL | Custom keywords field |
| URL | rdf:http://home.netscape.com/NC-rdf#URL | The URL to link to |
| Possible Bookmarks Roots | |
| NC:BookmarksRoot | The top level of the bookmarks hierarchy |
| NC:IEFavoritesRoot | The bookmark folder that corresponds to the user's IE favorites. |
| NC:PersonalToolbarFolder | The bookmark folder that corresponds to the personal toolbar folder |
Files (rdf:files): A view of the user's files.
| Resources | ||
| Name | rdf:http://home.netscape.com/NC-rdf#Name | Name of the file |
| URL | rdf:http://home.netscape.com/NC-rdf#URL | URL of the file |
| Possible Files Roots | |
| NC:FilesRoot | Top level of the filesystem (usually the list of drives) |
| A file URL | By using a file URL for the ref attribute, you can select a specific directory to be returned. For example, you might use file:///windows or files:///usr/local. |
The files datasource is an example of a datasource that determines its resources only when necessary. We don't want every file in the filesystem to be determined before the data is displayed. Instead, only the files and directories that the tree element (or other elements) will need to display at a given time will be determined.
You can specify multiple datasources in the datasources attribute by separating them with whitespace as in the example below. This has the effect of reading the data from all the datasources mentioned.
<tree datasources="rdf:bookmarks rdf:history animals.rdf" ref="NC:BookmarksRoot">
This example reads the resources from the bookmarks, history and the animals.rdf file. They are combined into a single composite datasource and can be used as if they were one.
The special datasource rdf:null corresponds to nothing. You can use this datasource if you want to dynamically set the datasource using a script, but don't want one initially or don't know its exact URL.
You can use any of the above internal datasources if you wish. There are several others for mail, address books and searching and so on. However, you might want to use your own RDF datasource stored in an RDF file. The file can be either a local file or a remote file. Just put the URL of the RDF file in the datasources attribute.
Using RDF files provides just as much functionality as any of the internal datasources. You can use rules to match specific types of content. The attributes on the rule element will match if they match the attributes on an RDF Description element. You can also create RDF files that are hierarchical.
The following is an example of how an RDF file can be used as a datasource. The RDF file is fairly large and can be viewed separately: Source RDF
Example 6.8.2: Source View<tree flex="1" width="200" height="200"
datasources="animals.rdf" ref="urn:animals:data">
<treecols>
<treecol id="name" label="Name" primary="true" flex="1"/>
<treecol id="species" label="Species" flex="1"/>
</treecols>
<template>
<rule>
<treechildren>
<treeitem uri="rdf:*">
<treerow>
<treecell label="rdf:http://www.some-ficticious-zoo.com/rdf#name"/>
<treecell label="rdf:http://www.some-ficticious-zoo.com/rdf#species"/>
</treerow>
</treeitem>
</treechildren>
</rule>
</template>
</tree>
Here, the data has been generated from the file. The ref attribute has been set to the root element in the RDF file, which is the top-level Seq. This will give us a complete list of animals. If we wanted to, we could set the ref attribute to any of the other about attribute values to limit the set of data that is returned. For example, to display only the reptiles, use a value of urn:animals:reptiles.
The example below shows how to display a particular piece of an RDF datasource by setting the ref attribute.
Example 6.8.3: Source View<window
id="example-window"
title="History List"
xmlns:ANIMALS="http://www.some-ficticious-zoo.com/rdf#"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<button label="Click here to see the mammals the zoo has" type="menu"
datasources="animals.rdf" ref="urn:animals:mammals">
<template>
<rule ANIMALS:specimens="0"></rule>
<rule>
<menupopup>
<menuitem uri="rdf:*" label="rdf:http://www.some-ficticious-zoo.com/rdf#name"/>
</menupopup>
</rule>
</template>
</button>
</window>In this case only the mammals are desired, so we select the URI of the mammals list. You will notice that the value of the ref attribute in the example is urn:animals:mammals which corresponds to one of the Seq elements in the RDF file. This causes only the descendants of this list to be returned.
Two rules have been used here. The first rule catches all the resources that have their ANIMALS:specimens attribute set to 0. You can see this attribute in the RDF file on each of the Description elements. Some of them have a value of 0. So in these cases, rule one will match. Because rule one has no content, nothing will be displayed for these ones. This is an effective way to hide data that we don't want to display.
The second rule applies to all other resources and creates a row in a popup menu. The end effect is that we get a popup menu containing all the mammals which have a specimen that is not 0.
This section describes the more advanced rule syntax.
The rule syntax described so far is useful for some datasources but sometimes you will need to display data in more complicated ways. The simple rule syntax is really just a shortcut for the full rule syntax which is described below. Like the simple rules, full rules are placed within the rule tag.
Full rules contain three child tags, a conditions tag, a bindings tag and an action tag, although the bindings tag is not always needed.
The conditions element is used to specify the criteria for matching a given resource. You can specify a number of conditions, all of which must match. In the simple rule syntax, the conditions are placed directly on the rule element itself.
If the conditions match for a resource, the content placed within the actions tag is generated. In the simple syntax, the content is placed directly inside the rule.
When a tree, menu or other element with a datasource generates content, the template builder first finds the resource referred to by the ref attribute. It then iterates over all that resource's child resources. It applies each resource to the conditions. If the conditions match for that resource, the content in the actions element is generated for that resource. If the conditions do not match, no content is generated.
The conditions element can contain three elements. The first is the content element, which should always exist once and only once. It serves as a placeholder as the template builder iterates through the resources. It specifies the name of a variable in which is placed a reference to the root resource while the conditions are analyzed for a match. The root resource is the one specified by the ref attribute on the element containing the template.
The syntax of the content element is as follows:
<content uri="?var"/>
The question mark indicates that the text following it is a variable. You can then use the variable 'var' within the remainder of the conditions. Of course, you can name the variable whatever you want.
The next element is the member element, which is used to iterate through a set of child resources. In RDF terms, that means a container such a Seq, Bag or Alt. Let's say you have a list of cities described in the following RDF fragment:
<RDF:Seq about="urn:cities"> <RDF:li resource="urn:cities:Paris"/> <RDF:li resource="urn:cities:Manchester"/> <RDF:li resource="urn:cities:Melbourne"/> <RDF:li resource="urn:cities:Kiev"/> </RDF:Seq> <RDF:Description about="urn:cities> <cityset:name>Paris</cityset:name> </RDF:Description> . . .
You want to display a row in a tree for each city. To do this, use the member element as in the following:
<tree id="citiesTree" datasources="weather.rdf" ref="urn:cities">
<template>
<rule>
<conditions>
<content uri="?list"/>
<member container="?list" child="?city"/>
</conditions>
<rule>
<template>
</tree>The template builder starts by grabbing the value of the ref attribute, which in this case is urn:cities. This resource will be placed in the 'list' variable as specified by the content tag. We can then get related resources to the root resource by using the 'list' variable.
The template builder then sees the member element. It causes the builder to iterate over the children of an element. The parent is specified by the container attribute and the children are specified by the child attribute. In the example above, the value of the container attribute is the variable 'list'. Thus the parent will be the value of the list variable, which has been set to the root resource 'urn:cities'. The effect will be to iterate through the list of children of 'urn:cities'.
If you look at the RDF above, the 'urn:cities' resource has four children, one for each different city. The template builder iterates through each one, matching the child against the value of the child attribute. In this case, it is just set to the variable 'city'. So the builder will set the 'city' variable to the each child resource in turn.
Because there are no more conditions, the condition matches for each of those four resources and the builder will generate content for each of the four. Of course, the example above doesn't have any content. We'll add that later.
The next element is the triple element. It is used to check for the existence of a given triple (or assertion) in the RDF datasource. A triple is like a property of a resource. For example, a triple exists between a bookmark and its URL. This might be expressed as follows:
A Bookmark to mozilla.org --> URL --> www.mozilla.org
This means that there is a triple between the bookmark 'A Bookmark to mozilla.org' and 'www.mozilla.org' by the URL property. The first part of this expression is called the subject, the second part is called the predicate and the last part is called the object. As a triple element, it would be expressed as follows:
<triple subject="A Bookmark to mozilla.org"
predicate="URL"
object="www.mozilla.org"/>This has been simplified a bit from the real thing. The predicate would normally include the namespace, and the subject would be the bookmark's resource id, not the bookmark's title as used here. In fact, the bookmark's title would be another triple in the datasource using the Name predicate.
You can replace the subject and object on the triple element with variable references, in which case values will be substituted for the variables. If no value is defined for a variable yet, the template builder will look up the value in the datasource and assign it to the variable.
Let's say we wanted to add a weather prediction to the city datasource. The following conditions might be used:
<conditions>
<content uri="?list"/>
<member container="?list" child="?city"/>
<triple subject="?city"
predicate="http://www.xulplanet.com/rdf/weather#prediction"
object="?pred"/>
</conditions>The template builder will iterate over each city as before. When it gets to the triple, it will look for an assertion in the RDF datasource for a city's weather prediction. The variable 'pred' will be assigned the prediction. The builder will repeat this for each of the four cities. A match occurs and the builder will generate content for each city that has a prediction. If the city has no prediction resource, the condition does not match and no content will be generated for that city. Note that you do not need to put 'rdf:' at the beginning of the predicate, as that part is assumed.
We could also replace the object with an in-line value. For example:
<conditions>
<content uri="?city"/>
<triple subject="?city"
predicate="http://www.xulplanet.com/rdf/weather#prediction"
object="Cloudy"/>
</conditions>This example is similar but we specify that we want to match on 'Cloudy'. The result is that the conditions will only match for cities where the prediction is 'Cloudy'.
We can add more triples to require more matches. For example, in the example above, we might want to check for the temperature and the wind speed. To do this just add another triple which checks for the additional resource. The condition will match if all of the triples provide values.
The example below will check for an extra triple for the name of the city. It will be assigned to the 'name' variable. The condition will only match if the city has both a name and a prediction.
<conditions>
<content uri="?list"/>
<member container="?list" child="?city"/>
<triple subject="?city"
predicate="http://www.xulplanet.com/rdf/weather#prediction"
object="?pred"/>
<triple subject="?city"
predicate="http://www.xulplanet.com/rdf/weather#name"
object="?name"/>
</conditions>The content to generate for a rule is specified inside the action element. This should be the content for the rows of the tree, menu items, or whatever content you want to generate. Within the content, you can refer to variables that were defined in the conditions. Thus, in the weather example above, you could use the variables 'name' or 'pred' to display the city or prediction. You can use the 'list' or 'city' variables also, but they hold resources, not text, so they won't likely have meaningful values to users.
In the simple rule syntax, you use the syntax uri='rdf:*' to indicate where content should be generated. In the full syntax, you set the value of the uri attribute to a variable which you used in the conditions. Usually, this will be the variable assigned in the child attribute of the member element.
The following example shows a complete tree with conditions and an action. You can view the RDF file separately. Source RDF
Example 6.9.1: Source<tree id="weatherTree" flex="1" datasources="weather.rdf" ref="urn:weather:cities">
<treecols>
<treecol id="city" label="City" primary="true" flex="1"/>
<treecol id="pred" label="Prediction" flex="1"/>
</treecols>
<template>
<rule>
<conditions>
<content uri="?list"/>
<member container="?list" child="?city"/>
<triple subject="?city"
predicate="http://www.xulplanet.com/rdf/weather#prediction"
object="?pred"/>
<triple subject="?city"
predicate="http://www.xulplanet.com/rdf/weather#name"
object="?name"/>
</conditions>
<action>
<treechildren>
<treeitem uri="?city">
<treerow>
<treecell label="?name"/>
<treecell label="?pred"/>
</treerow>
</treeitem>
</treechildren>
</action>
</rule>
</template>
</tree>Two columns appear in this tree, one which displays the value of the name for each row and the other which displays the value of the prediction.
The final element you can add inside a rule is the bindings element. Inside it, you place one or more binding elements. A binding in a rule has the same syntax as a triple and performs almost the same function. For example, in the weather example above we could add the following binding:
<bindings>
<binding subject="?city"
predicate="http://www.xulplanet.com/rdf/weather#temperature"
object="?temp"/>
</bindings>This binding will grab the temperature resource of each city and assign it to the 'temp' variable. This is similar to what a triple does. The difference is that a binding is not examined when attempting to check the conditions. This means that the city must have a name and prediction to be displayed, yet it does not matter if it has a temperature. However, if it does, it will be placed in the 'temp' variable so it can be used in the action. If a city does not have a temperature, the 'temp' variable will be set to an empty string.
The find files dialog so far looks quite good. We haven't cleaned it up much but we have created a simple user interface easily. Next, we will show how to add scripts to it.
To make the find files dialog functional, we need to add some scripts which will execute when the interacts with the dialog. We would want to add a script to handle the Find button, the Cancel button and to handle each menu command. We write this using JavaScript functions much in the same way as HTML.
If you know HTML, you may be able to guess how we make the buttons and other XUL elements respond to events. We can just add handlers such as onclick on the elements. Then we provide a script which executes the function.
You can use the script element to include scripts in XUL files. You can embed the script code directly in the XUL file in between the opening and closing script tags but it is much better to include code in a separate file. Your XUL window will load faster. Instead, we use the src attribute to link in an external script file.
Let's add a script to the find file dialog. Although it does not matter what the script file is called, usually it would be the same as the XUL file with a js extension. In this case, findfile.js will be used. Add the line below just after the opening window tag and before any elements.
<script src="findfile.js"/>We'll create the script file later when we know what we want to put it in it. We'll define some functions in the file and we can call them in event handlers.
Event handlers in XUL are done much in the same way as they are in HTML. The same event handlers are available plus some additional ones. To handle an event, simply add one of the attributes in the table below to an element. There are a number of other ones you can add also but we'll see those in later sections.
| onclick | Called when the mouse is pressed and released on an element. Normally, you would use oncommand to respond to menus and buttons, because it also catches users who use the keyboard or other devices. | ||
| onmousedown | Called when a mouse button is pressed down on an element. The event handler will be called as soon as a mouse button is pressed, even if it hasn't been released yet. | ||
| onmouseup | Called when a mouse button is released on an element | ||
| onmouseover | Called when the mouse pointer is moved onto an element. You could use this to highlight the element, however CSS provides a way to do this automatically so you shouldn't do it with an event. You might, however, want to display some help text on a status bar. | ||
| onmousemove | Called when the mouse pointer is moved while over an element. The event will be called many times if the user moves the mouse so you should try to avoid using this handler if you can. | ||
| onmouseout | Called when the mouse pointer is moved off of an element. You might then unhighlight the element or remove status text. | ||
| oncommand | This event is called when a button or menu item is selected. For menus, add this handler to the menuitem element. You should use this handler rather than handling the mouse yourself as the user might select the button or menu item with the mouse or by pressing the access key or keyboard shortcut. | ||
| onkeypress | Called when a key is pressed and released when an element has the focus. You might use this to add extra shortcut key handling or to check for allowed characters in a field. We'll see how to create keyboard shortcuts in a later section. | ||
| onkeydown | Called when a key is pressed down while an element has the focus. Note that the event will be called as soon as the key is pressed, even if it hasn't been released. You probably won't use this event very often as the other key events are more suitable. | ||
| onkeyup | Called when a key is released while an element has the focus. | ||
| onfocus | Called when an element receives the focus either by clicking with the mouse or by using the TAB key. You might use this event to highlight the element or display some help text. | ||
| onblur | Called when an element loses the focus either by a user clicking on another element or by pressing TAB. You might use this to verify information or close popups. It is better to verify fields when the OK button is clicked however. | ||
| onload | Called on a window when it first opens. You would usually add this event handler to a window tag in order to initialize a window. This would allow fields to be set to default values based on conditions contained in a script. | ||
| onunload | Called when the window is closing. You would usually add this to the window tag to record information before the window closes. | ||
The oncommand is the handler you will usually use to respond to respond to buttons and menus. You can put code here which is executed when the user clicks on the element, or activates it with the keyboard.
An oncommand handler can be placed on the Find and Cancel buttons in the file files dialog. Clicking the Find button should start the search. Because we aren't going to implement this part, we'll leave it out. However, clicking the Cancel button should close the window. The code below shows how to do this. While we're at it, let's add the same code to the Close menu item.
<menuitem label="Close" accesskey="c" oncommand="window.close();"/> ... <button id="cancel-button" label="Cancel" oncommand="window.close();"/>
Two handlers have been added here. The oncommand handler was added to the Close menu item. By using this handler, the user will be able to close the window by clicking the menu item or by selecting it with the keyboard. The oncommand handler was added to the Cancel button.
Similarly, we can respond to other mouse events and keyboard presses using the mouse and key event handlers. If you do not specify an event handler for a particular event, the element will handle it itself. This is usually what you want to have happen. Most XUL elements will have their own responses to mouse and keyboard events. For example, menuitems will respond to their access keys and popups will appear at the right time.
XUL uses the same event model that is described in DOM2 Events. Briefly, an event is sent in two phases. In the capturing phase, an event such as a mouse click is first sent to the document and then it works its way down the hierarchy of element until its reaches the element that triggered the event. The bubbling phase occurs next which is the reverse. The event is sent back up the hierarchy to each element in turn.
Along the way, if the event is handled by an element, processing of the event stops. If that element does not handle the event, it continues to the next element until one does. If nothing handles the event, default processing is performed.
This means that you do not have to put the event handler on the element that you want to respond to the event. You can also put it on any parent element above the element. For example, instead of putting the event handler on the menuitem, you could put the handler on the menu element.
A handler should return true if the event has been handled or false if the event has not been handled. By returning false, you can do something but still have the default processing occur. This process is similar to how HTML events are handled.
You can get the element that the event was originally passed to by getting the target property of the event object. For example, if you add the following event handler to a window, an alert box will appear whenever you click on an element in the window. The alert will display which element was clicked on.
<window
onclick="alert(event.target.tagName); return false;"
.
.
.
>You can also add event handlers dynamically using the function addEventListener. You can use it for any element and event type. It takes three parameters, the event type, a function to execute when the event occurs and a boolean indicating whether to capture or not.
The Document Object Model (DOM) can be used with XUL elements to get information about them or modify them.
XUL elements support the DOM properties and methods which will allow you to get information about elements and modify any element. A full discussion of DOM features will not provided here but some examples will be provided.
The id attribute can be used to identify elements. We've added the id attribute to a number of elements in the find file dialog. For example, we could get the state of the Case Sensitive Search check box by using the code below:
var state=document.getElementById('casecheck').checked;The value casecheck corresponds to the id of the case sensitive check box. Once we have an indication of whether it is checked or not, we can use the state to perform the search. We could do something similar for the other check box, or any other element that has an id. We'll need to get the text in the input field for example.
It doesn't make sense to have the progress bar and the dummy tree data displayed when the find files dialog is first displayed. These were added so that we could see them. Let's take them out now and have them displayed when the Find button is pressed. First, we need to make them initially invisible. The CSS property display is used to control whether an element is visible or not.
We'll change the progress meter so that is initially hidden. Also, we will add an id attribute so that we can refer to it in a script to show and hide it. While we're at it, let's also hide the splitter and results tree as we only need to show them after a search is performed.
<tree id="results" style="display: none;" flex="1"> . . . <splitter id="splitbar" resizeafter="grow" style="display:none;"/> <hbox> <progressmeter id="progmeter" value="50%" style="margin: 4px; display: none;"/>
We've added a CSS style property display and set its value to none. This causes the element to be hidden when it first appears. By default, the display property defaults to a value which tells the element to be displayed. The exact value is different for each element, because each element has a different way of being displayed. Look in the file xul.css in the Mozilla chrome directory for details about the default styles for each XUL element and the file html.css in the Mozilla res directory for details about the default styles for each HTML element.
First, let's add a function that is called when the Find button is pressed. We'll put scripts in a separate file findfile.js. In the last section, we added the script element in the XUL file. If you haven't done this, do that now, as shown below. An oncommand handler will also be added to the Find button.
<script src="findfile.js"/>
.
.
.
<button id="find-button" label="Find" default="true"
oncommand="doFind();"/>Now, create another file called findfile.js in the same directory as findfile.xul. We'll add the doFind() function is this file. The script tag does allow code to be contained directly inside of it. However, for various reasons, including better performance, you should always put scripts in separate files, except for short fragments which can be put directly in the event handler.
function doFind()
{
var meter=document.getElementById('progmeter');
meter.setAttribute("style","margin: 4ex; display: -moz-xul-box;");
}This function first gets a reference to the progress meter using its id, progmeter. The second line of the function body sets the style attribute of the progress meter to have a display of '-moz-xul-box', which is the value for XUL boxes and elements based on them. Note that we've also had to add the margin so that it doesn't get removed.
Finally, let's have an alert box pop up that displays what will be searched for. Of course, we wouldn't want this in the final version but we'll add it to reassure us that something would happen.
function doFind()
{
var meter=document.getElementById('progmeter');
meter.setAttribute("style","display: inline;");
var searchtext=document.getElementById('find-text').value;
alert("Searching for \""+searchtext+"\"");
}Now, with that alert box in there, we know what should happen when we click the Find button. We could add additional code to get the selections from the drop-down boxes too.
You can create new elements using the createElement function of the document. It takes one argument, the tag name of the element to create. You can then set attributes of the element using the setAttribute function and append it to the XUL document using the appendChild function. For example, the following will add a button to a XUL window:
Example 7.2.1: Source View
<script>
function addButton()
{
var aBox = document.getElementById("aBox");
var button = document.createElement("button");
button.setAttribute("label","A Button");
aBox.appendChild(button);
window.sizeToContent();
}
</script>
<box id="aBox">
<button label="Add" oncommand="addButton();"/>
</box>This script first gets a reference to the box, which is what we will add a new button to. The createElement function creates a new button. We assign a label 'A Button' to the button using the setAttribute function. The appendChild function of the box is called to add the button to it. Finally, we call the sizeToContent method of the window, which causes the window to resize to fit the newly added button.
For a more complete list of functions available for XUL elements, see the element reference.
You could use keyboard event handlers to respond to the keyboard. However, it would be tedious to do that for every button and menu item.
XUL provides methods in which you can define keyboard shortcuts. We've already seen in the section on menus that we can define an attribute called accesskey which specifies the key which a user can press to activate the menu or menu item. In the example below, the File menu can be selected by pressing Alt and F (or some other key combination for a specific platform). Once the File menu is open, the Close menu item can be selected by pressing C.
Example 7.3.1: Source View<menubar id="sample-menubar">
<menu id="file-menu" label="File" accesskey="f">
<menupopup id="file-popup">
<menuitem id="close-command" label="Close" accesskey="c"/>
</menupopup>
</menu>
</menubar>You can also use the accesskey attribute on buttons. When the key is pressed in this case, the button is selected.
You might want to set up more general keyboard shortcuts however. For example, pressing Contol+C to copy text to the clipboard. Although shortcuts such as this might not always be valid, they will usually work any time the window is open. Usually, a keyboard shortcut will be allowed at any time and you can check to see whether it should do something using a script. For example, copying text to the clipboard should only work when some text is selected.
XUL provides an element, key, which lets you define a keyboard shortcut for a window. It has attributes to specify the key that should be pressed and what modifier keys (such as Shift or Control) need to be pressed. An example is shown below:
<key id="sample-key" modifiers="shift" key="R"/>
This sample defines a keyboard shortcut that is activated when the user presses the Shift key and R. The key attribute (note that it has the same name as the element itself) can be used to indicate which key should be pressed, in this case R. You could add any character for this attribute to require that key to be pressed. The modifiers that must be pressed are indicated with the modifiers attribute. It is a space-separated list of modifier keys, which are listed below.
Your keyboard won't necessary have all of the keys, in which case they will be mapped to modifier keys that you do have.
Each platform generally uses a different key for keyboard shortcuts. For example, Windows uses the Control key and the Macintosh uses the Command key. It would be inconvenient to define separate key elements for each platform. Luckily, there is a solution. The modifier accel refers to the special platform-specific key used for shortcuts. If works just like the other modifiers, but won't be the same on every platform.
Here are some additional examples:
<key id="copy-key" modifiers="control" key="C"/> <key id="explore-key" modifiers="control alt" key="E"/> <key id="paste-key" modifiers="accel" key="V"/>
The key attribute is used to specify the key that must be pressed. However, there will also be cases where you want to refer to keys that cannot be specified with a character (such as the Enter key or the function keys). The key attribute can only be used for printable characters. Another attribute, keycode can be used for non-printable characters.
The keycode attribute should be set to a special code which represents the key you want. A table of the keys is listed below. Not all of the keys are available on all keyboards.
| VK_CANCEL | VK_BACK | VK_TAB | VK_CLEAR |
| VK_RETURN | VK_ENTER | VK_SHIFT | VK_CONTROL |
| VK_ALT | VK_PAUSE | VK_CAPS_LOCK | VK_ESCAPE |
| VK_SPACE | VK_PAGE_UP | VK_PAGE_DOWN | VK_END |
| VK_HOME | VK_LEFT | VK_UP | VK_RIGHT |
| VK_DOWN | VK_PRINTSCREEN | VK_INSERT | VK_DELETE |
| VK_0 | VK_1 | VK_2 | VK_3 |
| VK_4 | VK_5 | VK_6 | VK_7 |
| VK_8 | VK_9 | VK_SEMICOLON | VK_EQUALS |
| VK_A | VK_B | VK_C | VK_D |
| VK_E | VK_F | VK_G | VK_H |
| VK_I | VK_J | VK_K | VK_L |
| VK_M | VK_N | VK_O | VK_P |
| VK_Q | VK_R | VK_S | VK_T |
| VK_U | VK_V | VK_W | VK_X |
| VK_Y | VK_Z | VK_NUMPAD0 | VK_NUMPAD1 |
| VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 | VK_NUMPAD5 |
| VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 |
| VK_MULTIPLY | VK_ADD | VK_SEPARATOR | VK_SUBTRACT |
| VK_DECIMAL | VK_DIVIDE | VK_F1 | VK_F2 |
| VK_F3 | VK_F4 | VK_F5 | VK_F6 |
| VK_F7 | VK_F8 | VK_F9 | VK_F10 |
| VK_F11 | VK_F12 | VK_F13 | VK_F14 |
| VK_F15 | VK_F16 | VK_F17 | VK_F18 |
| VK_F19 | VK_F20 | VK_F21 | VK_F22 |
| VK_F23 | VK_F24 | VK_NUM_LOCK | VK_SCROLL_LOCK |
| VK_COMMA | VK_PERIOD | VK_SLASH | VK_BACK_QUOTE |
| VK_OPEN_BRACKET | VK_BACK_SLASH | VK_CLOSE_BRACKET | VK_QUOTE |
| VK_HELP |
For example, to create a shortcut that is activated when the user presses Alt and F5, do the following:
<key id="test-key" modifiers="alt" keycode="VK_F5"/>
You may add the key element wherever you want in a XUL window declaration, although it should be should be inside a keyset element. This element is designed for holding a set of key elements, which serves to group all of the key definitions in one place in a file. The example below demonstrates some more keyboard shortcuts:
<keyset> <key id="copy-key" modifiers="accel" key="C"/> <key id="find-key" keycode="VK_F3"/> <key id="switch-key" modifiers="control alt" key="1"/> </keyset>
The first key is invoked when the user presses their platform-specific shortcut key and C. The second is invoked when the user presses F3. The third is invoked on a press of the Control key, the Alt key and 1. If you wanted to distinguish between keys on the main part of the keyboard and the numeric keypad, use the VK_NUMPAD keys (such as VK_NUMPAD1).
Now that we know how to define keyboard shortcuts, we'll find out how we can use them. There are two ways. The first is the simplest and just requires that you use the regular key event handlers on the key element. When the user presses the key, the script will be invoked. An example is shown below:
<keyset> <key id="copy-key" modifiers="accel" key="C" onkeypress="DoCopy();"/> </keyset>
The function DoCopy will be called when the user presses the keys specified by the key element, which in this example, are the keys for copying to the clipboard (such as Control+C). This will work as long as the window is open. The DoCopy function should check to see if text is selected and then copy the text to the clipboard. Note that textboxes have the clipboard shortcuts built-in so you don't have to implement them yourself.
You can also use the keyup and keydown event handlers.
If you are assigning a keyboard shortcut that performs a command that also exists on a menu, you can associate the key element directly with the menu command. To do this, add a key attribute on the menuitem. Set its value to the id of the key that you want to use. The example below demonstrates this.
Example 7.3.2: Source View<keyset>
<key id="paste-key" modifiers="accel" key="V"
oncommand="alert('Paste invoked')"/>
</keyset>
<menubar id="sample-menubar">
<menu id="edit-menu" label="Edit" accesskey="e">
<menupopup id="edit-popup">
<menuitem id="paste-command" accesskey="p" key="paste-key" label="Paste"
oncommand="alert('Paste invoked')"/>
</menupopup>
</menu>
</menubar>The menuitem's key attribute, which here is paste-key is equal to the id of the defined key. You can use this for additional keys as well to define keyboard shortcuts for any number of menu items.
You'll also notice in the image that text has been placed next to the Paste
menu command to indicate that Control and the V key can be pressed to invoke the
menu command. This is added for you based on the modifiers on the
key element. Keyboard shortcuts attached to menus
will work even if the menu is not open.
One additional feature of key definitions is that you can disable them easily. To do this add a disabled attribute to the key element and set it to the value true. This disables the keyboard shortcut so that it cannot be invoked. It is useful to change the disabled attribute using a script.
Let's add keyboard shortcuts to the find files dialog. We'll add four of them, one for each of the Cut, Copy, and Paste commands and also one for the Close command when the user presses Escape.
<keyset> <key id="cut_cmd" modifiers="accel" key="X"/> <key id="copy_cmd" modifiers="accel" key="C"/> <key id="paste_cmd" modifiers="accel" key="V"/> <key id="close_cmd" keycode="VK_ESCAPE" oncommand="window.close();"/> </keyset> <vbox flex="1"> <toolbox> <menubar id="findfiles-menubar"> <menu id="file-menu" label="File" accesskey="f"> <menupopup id="file-popup"> <menuitem label="Open Search..." accesskey="o"/> <menuitem label="Save Search..." accesskey="s"/> <menuseparator/> <menuitem label="Close" accesskey="c" key="close_cmd" oncommand="window.close();"/> </menupopup> </menu> <menu id="edit-menu" label="Edit" accesskey="e"> <menupopup id="edit-popup"> <menuitem label="Cut" accesskey="t" key="cut_cmd"/> <menuitem label="Copy" accesskey="c" key="copy_cmd"/> <menuitem label="Paste" accesskey="p" key="paste_cmd" disabled="true"/> </menupopup> </menu>
Now we can use those shortcuts to activate the commands. Of course, the clipboard commands don't do anything anyway, as we haven't written those scripts.
The section will describe how to handle the focus and selection of elements.
The focused element refers to the element which currently receives input events. If there are three textboxes on a window, the one that has the focus is the one that the user can currently enter text into. Only one element per window has the focus at a time.
The user can change the focus by clicking an element with the mouse or by pressing the TAB key. When the TAB key is pressed, the next element is given the focus.
The focus event is used to respond when the focus is given to an element. The blur event is used to respond when the focus is removed from an element. You can respond by adding an onfocus or onblur attribute on the element. They work just like their HTML counterparts. You might use these event handlers to highlight the element or display text on a status bar.
You can also add event handlers dynamically using the DOM function addEventListener. You can use it for any element and event type. It takes three parameters, the event type, a function to execute when the event occurs and a boolean indicating whether to capture or not. The following example can be used to apply a function to handle a focus event.
Example 7.4.1: Source View<script>
function displayFocus()
{
var elem=document.getElementById('sbar');
elem.setAttribute('value','Enter your phone number.');
}
</script>
<textbox id="tbox1"/>
<textbox id="tbox2" onfocus="displayFocus();"/>
<description id="sbar" value=""/>
In this example, the Init function is called when the page has loaded. The addEventListener function adds a focus event handler to the second textbox. This event, when it occurs, will call the displayFocus function. This function will change the value of the text label. We could extend this example to remove the text when the blur event occurs.
You can call the focus method of XUL elements to set the focus on a particular element. The blur method can be used to remove it. The following example demonstrates this:
Example 7.4.2: Source View<textbox id="addr"/>
<button label="Focus" onclick="document.getElementById('addr').focus()"/>For textboxes, a special attribute, focused is added whenever the element has the focus. You can check for the presence of this attribute to determine if the element has the focus, either from a script or within a style sheet. It will have the value true. if the textbox has the focus and, if the textbox does not have the focus, the attribute will not be present.
When working with a textbox, you may wish to retrieve not the entire contents of a field but only what the user has selected. Or, you may wish to change the current selection.
XUL textboxes support a way to retrieve and modify the selection. The simplest one is to select all of the text in a textbox. This involves using the select method of the textbox.
tbox.select();
However, you may wish to select only part of the text. To do this you can use the setSelectionRange function. It takes two parameters, the first is the starting character and the second is the character after the last one that you want to have selected. Values are zero-based, so the first character is 0, the second is 1 and so on.
tbox.setSelectionRange(4,8);
This example will select the fifth character displayed, as well as the sixth, seventh and eighth. If there were only six characters entered into the field, only the fifth and sixth characters would be selected. No error would occur.
If you use the same value for both parameters, the start and end of the selection changes to the same position. This results in changing the cursor position within the textbox. For example, the line below can be used to move the cursor to the beginning of the text.
tbox.setSelectionRange(0,0);
You can retrieve the current selection by using the selectionStart and selectionEnd properties. These properties are set to the starting and ending positions of the current selection respectively. If both are set to the same value, no text is selected, but the values will be set to the current cursor position. Once you have the start and end positions, you can pull out the substring from the whole text.
You can retrieve and modify the contents of the textbox by using the value property.
One additional useful property of textboxes is the textLength property, which holds the total number of characters in the field.
Several other elements provide methods to retrieve the selected item within it. Listboxes, menulists and tabboxes provide a selectedIndex property that you can use to return the index of the currently selected item in the list or menu, or the currently selected tab panel.
If you assign a value to the selectedIndex property, the currently selected item is changed.
The section will describe how to get and set the selected items in a tree.
Each treeitem element in a tree may be selected individually. If you add the seltype attribute to the tree, set to the value multiple, the user can select multiple items at a time. The selection is not necessarily contiguous. The tree provides a number of functions which can be used to determine whether an item is selected.
First, let's see how we can determine when an item is selected. The onselect event handler may be added to the tree element. When the user selects an item from the tree, the event handler is called. The user may also change the selection by using the cursor keys. If the user holds down the cursor key to rapidly scroll through the items, the event handler is not called until the user stops. This results in a performance improvement. This also means that the highlight will appear on several items even though the select event is never fired for those items.
The syntax of the onselect event handler is shown below.
<tree id="treeset" onselect="alert('You selected something!');">The tree has a property currentIndex, which can be used to get the currently selected item, where the first row is 0. You can change the selected item by assigning a new value to this property.
Child items are included in the count just after their parents. This means that if there are 3 top-level items and each has two child items, there will be a total of 6 items. The first item (at index 0) will be the first top-level item. The next item at index 1 will be its first child. The second child will be at index 2 and the second parent item will be at position 3 and so on.
In the image to the right, there are eight rows displayed, of which two are selected. The first selected row has an index of 4 and the second has an index of 7. The rows that are not displayed are not included in the index count.
For trees that allow multiple selection, getting the list of selected rows is a bit more complicated. The tree element has a property view which in turn has a property selection, which contains properties and methods related to the current selection in the tree. You can use these methods to retrieve the set of selected items or modify the selection.
Because the selected items in a multiple selection tree are not necessarily contiguous, you can retrieve each block of contigous selections, using the getRangeCount and getRangeAt functions. The first function returns the number of selection ranges there are. If only one value is selected, this value will be 1. You would then write a loop for the number of ranges, calling getRangeAt to get the actual indices of the start and end of the range.
The getRangeAt function takes three arguments. The first is the range index. The second is an object which will be filled in by the function with the index of the first selected item. The third argument is an object which will be filled in with the index of the last selected item.
For example:
var start = new Object();
var end = new Object();
var numRanges = tree.view.selection.getRangeCount();
for (var t=0; t<numRanges; t++){
tree.view.selection.getRangeAt(t,start,end);
for (var v=start.value; v<=end.value; v++){
alert("Item "+v+" is selected.");
}
}We create two objects called 'start' and 'end'. Then, we iterate over the set of ranges, the number of which is returned by the getRangeCount function. The getRangeAt function is called passing the range index and the start and end objects. This function will fill in the start and end indicies by assigning them to the 'value' property. So if the first range is from the third item to the seventh item, 'start.value' will be 2 (remember that indices start with 0, so we subtract one.) and 'end.value' will be 6. An alert is displayed for each index.
In addition to static content and RDF content, trees may also get their content from a custom view.
So far, we have seen two ways to specify the content of a tree. You may place static rows and cells inside a tree, or you use an RDF datasource. The first method works fine provided you have a small amount of data to present. Using a datasource is suitable when one is available, either one provided with Mozilla or one you create yourself. In some cases, you may want to store the data is some other format, or perform computations on the data. XUL provides a third method, which involves creating a custom view object with a script.
This method can be used to hold data for hundreds of thousands of rows which the tree can display instantly. To do this, implement a tree just as before but leave the treechildren element empty. The following example shows this:
<tree id="my-tree" flex="1">
<treecols>
<treecol id="namecol" label="Name" flex="1"/>
<treecol id="datecol" label="Date" flex="1"/>
</treecols>
<treechildren/>
</tree>To assign data to be displayed in the tree, a script object needs to be created which is used to indicate the value of each cell, the total number of rows plus other optional information. The script object should be assigned to the tree. The tree will call methods of the object to get the information that it needs to display.
The script object, called the tree view, supports thirty or so methods that you can implement to supply information about the tree content and its appearance, but you only have to implement a small number of them. Two methods that you should implement are listed below.
Here is an example of defining such as object, which can called whatever you want:
var treeView = {
rowCount : 10000,
getCellText : function(row,column){
if (column=="namecol") return "Row "+row;
else return "February 18";
},
};This example can be used for an tree with 10000 rows. The contents of the cells in the first column will be set to the text 'Row X' where X is the row number. The contents of the cells in the second column will be set to 'February 18'. The if statement in the function getCellText compares the column to the text 'namecol'. This text (namecol) corresponds to the id of the first treecol in the example above. This example is very simple of course -- in reality you would have more complex data in each cell.
The final step is to associate the view object with the tree. The tree has a property view, which can be assigned to the view object declared above. We can assign a value to this property at any time to set or change the view.
function setView()
{
document.getElementById('my-tree').view=treeView;
}The following presents the example together. An inline script has been used here to simplify the example. Normally, you would put the script in an external script file.
Example 7.6.1: Source<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window title="Tree Example" id="tree-window"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
onload="setView();">
<script>
var treeView = {
rowCount : 10000,
getCellText : function(row,column){
if (column=="namecol") return "Row "+row;
else return "February 18";
},
};
function setView()
{
document.getElementById('my-tree').view=treeView;
}
</script>
<tree id="my-tree" flex="1">
<treecols>
<treecol id="namecol" label="Name" flex="1"/>
<treecol id="datecol" label="Date" flex="1"/>
</treecols>
<treechildren/>
</tree>
</window>
In the image, you can see two columns, each with data taken from the getCellText function. The setView function has been called in the onload handler for the window. You could also set the view later if you wish. You can change the view at any time.
One thing to note is that the getCellText function is only called when necessary to display the contents. In the 10000 row example above, getCellText is only called for the cells that are currently displayed. It is called for other rows when the user scrolls through them. This makes the tree much more efficient.
The nsITreeView interface lists all of the properties and methods that you can implement for the tree view.
There may be times when you want several elements to respond to events or changes of state easily. To do this, we can use broadcasters.
Once you have built a large application, you will often have several elements that have duplicate functionality. For example, buttons on a toolbar might have equivalent functionality to menu items. In addition, there may also be keyboard shortcuts or popup menus that do the same thing.
Let's say that we want the Back action in a browser to be disabled. We would need to disable the Back command on the menu, the Back button on the toolbar, the keyboard shortcut (Alt+Left for example) and any Back commands on popup menus. Although we could write a script to do this, it is quite tedious. It also has the disadvantage that we need to know all of the places where a Back action could be. This would be a problem when we wanted to add a new one as we would have to update all of the scripts. It would be more convenient to simply disable the Back action and have all the elements that issue the Back action disable themselves.
XUL provides a solution to this using an element called a broadcaster. This element holds the disabled state of the Back button. Each Back action element (the menu items and toolbar buttons) would watch the broadcaster and, when the disabled state changes on it, they will change also.
The simplest broadcaster is shown below. You should always use an id attribute so that it can be referred to by other elements.
<broadcasterset> <broadcaster id="back_command" disabled="true"/> </broadcasterset>
Any elements that are watching the broadcaster will be modified automatically whenever the broadcaster has its disabled attribute changed. This results in these elements becoming disabled themselves.
Like the keyset element, the broadcasterset element serves as a placeholder for broadcasters. You should declare all your broadcasters inside a broadcasterset so that they are kept together.
Elements that are watching the broadcaster are called observers because they observe the state of the broadcaster. To make an element an observer, add an observes attribute to it. For example, to make a Back button an observer:
<button id="back_button" label="Back" observes="back_command"/>
The observes attribute has been placed on the button and its value has been set to the value of the id on the broadcaster we want to observe. Here the Back button will observe the broadcaster which has the id back_command, which is the one defined earlier.
If the value of the disabled attribute on the broadcaster changes, the observers will update the values of their disabled attributes also. The result is that the button will be disabled and enabled when the broadcaster state changes.
We could continue with additional elements. As many elements as you want can observe a single broadcaster. You can also have only one if you wanted to but that would accomplish very little. You should only use broadcasters when you need multiple elements that observe a property. Below, we define some additional observers:
<key id="back_key" modifiers="accel" keycode="VK_LEFT" observes="back_command"/> <menuitem id="back_menuitem" label="Back" observes="back_command"/>
Now all we need to do is change the disabled attribute on the broadcaster element and the Back button, menu command and the keyboard shortcut are all disabled at once.
You can use a broadcaster to observe any attribute that you wish. The observers will grab all the values of any attributes from the broadcasters whenever they change. We could modify the earlier example to the following:
Example 7.7.1: Source View<broadcasterset>
<broadcaster id="back_command" label="Back" disabled="true"/>
</broadcasterset>
<keyset>
<key id="back_key" modifiers="accel" key="[" observes="back_command"/>
</keyset>
<toolbox>
<menubar id="back-menubar">
<menu id="back_menu" observes="back_command"/>
</menubar>
</toolbox>
<button id="back_button" observes="back_command"/>
Now, it addition to the disabled attribute, each observer will grab the label attribute as well, as both attributes have been placed on the broadcaster. The result is that the label of both the button and the menu will be set to Back. The label on the key will change as well but it won't use the value for anything.
Each observer will grab the attributes from the broadcaster and add them to itself. If the attribute already exists, it will be overwritten. For example, you would get the same effect if you added a label attribute to the button above as if you left it off, because it would be changed by the broadcaster.
Whenever the value of any of the attributes on the broadcaster changes, the observers are all notified and they update their own attributes to match. Attributes of the observers that the broadcaster doesn't have itself are not modified. The only attribute that is not updated is the id attribute. You can also use your own custom attributes if you wish.
There is also a way in which we can be more specific about which attribute of the broadcaster we observe. This involves an observes element. Like its attribute counterpart, it allows you to define an element to be an observer. The observes element should be placed as a child of the element that is to be the observer. An example is shown below:
<broadcasterset> <broadcaster id="back_command" disabled="false"/> </broadcasterset> <button id="back_button" label="Back"> <observes element="back_command" attribute="disabled"/> </button>
Two attributes have been added. The first, element specifies the id of the broadcaster to observe. The second, attribute, specifies the attribute to observe. The result here is that when the disabled attribute on the broadcaster is changed, the state of the button is changed. The observes element does not change but instead the element it is inside changes, which in this case is a button.
If we change the value of the attribute, we can observe other attributes of the broadcaster.
There is an additional event handler that we can place on the observes element which is onbroadcast. The event is called whenever the observer notices a change to the attributes of the broadcaster that it is watching. An example is shown below.
Example 7.7.2: Source View<broadcasterset>
<broadcaster id="thingy_command" style="color: black"/>
</broadcasterset>
<button label="Test">
<observes element="thingy_command" attribute="style" onbroadcast="alert('Color changed');"/>
</button>
<button label="Observer"
onclick="document.getElementById('thingy_command').setAttribute('style','color: red');"
/>Two buttons have been created, one labeled Test and the other labeled Observer. If you click on the Test button, nothing special happens. However, if you click on the Observer button, two things happen. First, the button changes to red text and, second, an alert box appears with the message 'Color changed'.
What happens is the onclick handler on the second button is called when the user presses on it. The script here gets a reference to the broadcaster and changes the style of it to have a color that is red. The broadcaster is not affected by the style change because it doesn't display on the screen. However, the first button has an observer which notices the change in style. The element and the attribute on the observes tag detect the style change. The style is applied to the first button automatically.
Next, because the broadcast occured, the event handler onbroadcast is called. This results in an alert message appearing. Note that the broadcast only occurs if the attributes on the broadcaster element are changed. Changing the style of the buttons directly will not cause the broadcast to occur so the alert box will not appear.
If you tried duplicating the code for the first button several times, you would end up with a series of alert boxes appearing, one for each button. This is because each button is an observer and will be signaled when the style changes.
In this section, we'll take a brief look at XPCOM (Cross-platform Component Object Model), which is the Object system that Mozilla uses.
By using XUL we can build a complex user interface. We can attach scripts which modify the interface and perform tasks. However, there are quite a number of things that cannot be performed directly with JavaScript. For example, if we wanted to create a mail application, we would need to write scripts which would connect to mail servers to retrieve and send mail. JavaScript does not have the capability to do such things.
The only way to handle this would be to write native code that would get mail. We also need to have a way for our scripts to call the native code easily. Mozilla provides such a method which involves using XPCOM (Cross-platform Component Object Model).
Mozilla is constructed from a collection of components, each of which performs a certain task. For example, there is a component for each menu, button and element. The components are constructed from a number of definitions called interfaces.
An interface in Mozilla is a definition of a set of functionality that could be implemented by components. Components are what implement the code in Mozilla that does things. Each component implements the functionality as described by interfaces. A single component might implement multiple interfaces. And multiple components might implement the same interface.
Let's take an example of a file component. An interface would need to be created which describes properties and functions that can be performed on files. A file would need properties for its name, modification date and its size. Functions of a file would include moving, copying and deleting it.
The File interface only describes the characteristics of a file, it does not implement it. The implementation of the File interface is left to a component. The component will have code which can retrieve the file's name, date and size. In addition, it will have code which copies and renames it.
We don't care how the component implements it, as long as it implements the interface correctly. Of course, we'll have different implementations anyway, one for each platform. The Windows and Macintosh versions of a file component would be significantly different. However, they would both implement the same interface. Thus, we can use a component by accessing it using the functions we know from the interface.
In Mozilla, interfaces are preceded by 'nsI' so that they are easily recognized as interfaces. For example, the nsIAddressBook is the interface for interacting with an address book, nsISound is used for playing files and nsILocalFile is used for files.
XPCOM components are typically implemented natively, which means that they generally do things that JavaScript cannot do itself. However, there is a way in which you can call them, which we will see shortly. We can call any of the functions provided by the component as described by the interfaces it implements. For example, once we have a component, we can check if it implements nsISound, and, if so, we can play sound through it.
The process of calling XPCOM from a script is called XPConnect, which is a layer which translates script objects into native objects.
There are three steps to calling an XPCOM component.
Once you've done the first two steps, you can repeat the last step as often as necessary. Let's say we want to rename a file. For this we can use the nsILocalFile interface. The first step is getting a file component. Second, we query the file component and get the portion of it that implements the nsILocalFile interface. Finally, we call functions provided by the interface. This interface is used to represent a single file.
We've seen that interfaces are always named starting with 'nsI'. Components, however, are referred to using a URI syntax. Mozilla stores a list of all the components that are available in its own registry. A particular user can install new components as needed. It works much like plug-ins.
Mozilla provides a file component, that is, a component that implements nsILocalFile. This component can be referred to using the URI '@mozilla.org/file/local;1'. The component: URI scheme is used to specify a component. Other components can be referred to in a similar way.
The URI of the component can be used to get the component. You can get a component using JavaScript code like that below:
var aFile = Components.classes["@mozilla.org/file/local;1"].createInstance();
The file component is retrieved and stored in the aFile variable. Components in the example above refers to a general object that provides some component related functions. Here, we get a component class from the classes property. The classes property is an array of all of the available components. To get a different component, just replace the URI inside the square brackets with the URI of the component you want to use. Finally, an instance is created with the createInstance function.
However, at this point, we only have a reference to the file component itself. In order to call the functions of it we need to get one of its interfaces, in this case nsILocalFile. A second line of code needs to be added as follows:
var aFile = Components.classes["@mozilla.org/file/local;1"].createInstance(); var aLocalFile = aFile.QueryInterface(Components.interfaces.nsILocalFile);
The function QueryInterface is a function provided by all components which can be used to get a specific interface of that component. This function takes one parameter, the interface that you want to get. The interfaces property of the Components object contains a list of all the interfaces that are available. Here, we use the nsILocalFile interface and pass it as a parameter to QueryInterface. The result is that aLocalFile will be set to a reference to the part of the component that implements the nsILocalFile interface.
The two JavaScript lines above can be used to get any interface of any component. Just replace the component name with the name of the component you want to use and change the interface name. You can also use any variable names of course. For example, to get a sound interface, you can do the following:
var sound = Components.classes["@mozilla.org/sound;1"].createInstance(); var nsisound = sound.QueryInterface(Components.interfaces.nsISound);
XPCOM interfaces can inherit from other interfaces. The interfaces that inherit from others have their own functions and the functions of all the interfaces that they inherit from. All interfaces inherit from a top-level interface called nsISupports. It has one function supplied to JavaScript, QueryInterface, which we have already seen. Because the interface nsISupports is implemented by all components, the function QueryInterface function is available in every component.
Several components may implement the same interface. Typically, they might be subclasses of the original but not necessarily. Any component may implement the functionality of nsILocalFile. In addition, a component may implement several interfaces. It is for these reasons that two steps are involved in getting an interface to call functions through.
However, there is a shortcut we can use because we'll often use both of these lines together:
var aLocalFile = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
This will do the same thing as the two lines but in one line of code. It eliminates the need to create the instance and then query it for an interface in two separate steps.
If you call QueryInterface on an object and the requested interface is not supported by an object, null is returned. You should always check to ensure that a non-null value is returned as in the following:
var aFile = Components.classes["@mozilla.org/file/local;1"].createInstance(); if (!aFile) return false; aLocalFile=aFile.QueryInterface(Components.interfaces.nsILocalFile); if (!aLocalFile) return false;
Now that we have an object that refers to a component with the nsILocalFile interface, we can call the functions of nsILocalFile through it. The table below shows some of the properties and methods of the nsILocalFile interface.
| initWithPath | This method is used to initialize the path and filename for the nsILocalFile. The first parameter should be the file path, such as '/usr/local/mozilla' |
| leafName | The filename without the directory part |
| fileSize | The size of the file |
| isDirectory() | Returns true if the nsILocalFile represents a directory |
| delete(recursive) | Deletes a file. If the recursive parameter is true, a directory and all of its files and subdirectories will also be deleted. |
| copyTo(directory,newname) | Copies a file to another directory, optionally renaming the file. The directory should be a nsILocalFile holding the directory to copy the file to. |
| moveTo(directory,newname) | Moves a file to another directory, or renames a file. The directory should be a nsILocalFile holding the directory to move the file to. |
In order to delete a file we first need to assign a file to the nsILocalFile. We can call the method initWithPath to indicate which file we mean. Just assign the path of the file to this property. Next, we call the delete function. It takes one parameter which is whether to delete recursively. The code below demonstrates these two steps:
var aFile = Components.classes["@mozilla.org/file/local;1"].createInstance();
var aLocalFile = aFile.QueryInterface(Components.interfaces.nsILocalFile);
if (!aLocalFile) return false;
aLocalFile.initWithPath("/mozilla/testfile.txt");
aLocalFile.delete(false);This code will take the file at /mozilla/testfile.txt and delete it. Try this example by adding this code to an event handler. You should change the filename to an existing file that you have that you would like to delete.
In the functions table above, you will see two functions copyTo and moveTo. These two functions can be used to copy files and move files respectively. Note that they do not take a string parameter for the directory to copy or move to, but instead take an nsILocalFile. That means that you'll need to get two file components. The example below shows how to copy a file.
function copyFile(sourcefile,destdir)
{
// get a component for the file to copy
var aFile = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
if (!aFile) return false;
// get a component for the directory to copy to
var aDir = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
if (!aDir) return false;
// next, assign URLs to the file components
aFile.initWithPath(sourcefile);
aDir.initWithPath(destdir);
// finally, copy the file, without renaming it
aFile.copyTo(aDir,null);
}
copyFile("/mozilla/testfile.txt","/etc");Some XPCOM components are special components called services. You do not create instances of them because only one should exist. Services provide general functions which either get or set global data or perform operations on other objects. Instead of calling createInstance, you call getService to get a reference to the service component. Other than that, services are not very different from other components.
One such service provided with Mozilla is a bookmarks service. It allows you to add bookmarks to the user's current bookmark list. An example is shown below:
var bmarks = Components.classes["@mozilla.org/browser/bookmarks-service;1"].getService();
bmarks = bmarks.QueryInterface(Components.interfaces.nsIBookmarksService);
bmarks.addBookmarkImmediately("http://www.mozilla.org","Mozilla",0,null);First, the component "@mozilla.org/browser/bookmarks-service;1" is retrieved and its service is placed in the variable bmarks. We use QueryInterface to get the nsIBookmarksService interface. The function addBookmarkImmediately provided by this interface can be used to add bookmarks. The first two parameters to this function are the bookmark's URL and its title. The third parameter is the bookmark type which will normally be 0 and the last parameter is the character encoding of the document being bookmarked, which may be null.
This section provides an example of using XPCOM along with some additional interfaces.
The interface nsIWindowMediator allows access to all the other open Mozilla windows. You can use this to switch the focus to other windows. The list of open windows can be used as an RDF datasource. This allows you to create a Window menu with a list of the currently open windows in the application. The datasource for this is rdf:window-mediator. We can use this as in the following example:
Example 8.2.1: Source<toolbox>
<menubar id="windowlist-menubar">
<menu label="Window">
<menupopup id="window-menu" datasources="rdf:window-mediator" ref="NC:WindowMediatorRoot">
<template>
<rule>
<menuitem uri="rdf:*" label="rdf:http://home.netscape.com/NC-rdf#Name"/>
</rule>
</template>
</menupopup>
</menu>
</menubar>
</toolbox>A Window menu will be created with a list of all the open windows. Try this example by opening a number of browser windows and you'll see that they are all listed on the menu. This is fine for displaying a list of open windows, but we would like to enhance this so that clicking on the menu item will switch to that window. This is accomplished by using the nsIWindowMediator interface. We can use it to get a specific window and then switch the focus to it. The code below shows how to get a component which implements it:
var wmdata = Components.classes["@mozilla.org/rdf/datasource;1?name=window-mediator"].getService(); var wmediator = wmdata.QueryInterface(Components.interfaces.nsIWindowMediator);
This code retrieves a window mediator component. Note the name of the component. You can use a similar syntax to get any of the built-in datasources. The component we are using is the same one that handles the window-mediator RDF datasource. A number of functions are available via the nsIWindowMediator interface:
| getEnumerator(type) | Get an object that implements nsISimpleEnumerator which can be used to interate over a set of open windows. |
| getMostRecentWindow(type) | Returns the window that was recently used. You can specify a type to get a window of a certain type. |
| getWindowForResource(url) | Given an RDF resource, return a window described by it. This can be used to get a window from the window-mediator RDF datasource. |
| convertISupportsToDOMWindow(win) | Given a XPCOM window reference, normally returned through the window enumeration, return a JavaScript window object. |
The type parameter can be null which means windows of any type (which effectively means all windows). If you specify a type, you can filter out windows that you are not interested in. A window's type can be specified by adding a windowtype attribute to a window element. For example:
<window id="findfile-window" title="Find Files" windowtype="find-files" xmlns:html="http://www.w3.org/1999/xhtml" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
However, we want to use the function getWindowForResource, because we can get the resource from the datasource. In the earlier example, we generated the list of windows and added it to a menu via a template. The template generates an id attribute on each menuitem element. The value of this attribute can be used as the resource. That means that in order to switch the window focus, we need to do the following:
The example below shows how we might do this:
<toolbox>
<menubar id="windowlist-menubar">
<menu label="Window" oncommand="switchFocus(event.target);">
<menupopup id="window-menu" datasources="rdf:window-mediator" ref="NC:WindowMediatorRoot">
<template>
<rule>
<menuitem uri="rdf:*" label="rdf:http://home.netscape.com/NC-rdf#Name"/>
</rule>
</template>
</menupopup>
</menu>
</menubar>
</toolbox>
function switchFocus(elem)
{
var wmdata = Components.classes["@mozilla.org/rdf/datasource;1?name=window-mediator"].getService();
var wmediator = wmdata.QueryInterface(Components.interfaces.nsIWindowMediator);
var resource = elem.getAttribute('id');
switchwindow = wmediator.getWindowForResource(resource);
if (switchwindow){
switchwindow.focus();
}
}A command handler was added to the menu element which calls the function with a parameter of the element that was selected from the menu. The function switchFocus first gets a reference to a component which implements the window mediator interface. Next, we get the id attribute for the element. We can use the value of the id attribute as the resource. The function getWindowForResource takes the resource and returns a window that matches it. This window, stored in the switchwindow variable, is the same as the JavaScript window object. This means that you can call any of the functions provided by it, one of which is the focus function.
This section explains how to manipulate RDF with a script.
Templates can be used to extract data from an RDF datasource and build content based on that data. However, the datasources can be examined from a script. You can get the datasource from an element built from a template, and pick out individual resources from it. This also allows you to modify the datasource.
The XPCOM interface to RDF involves a number of interfaces. The following lists some of the interfaces invloved:
| nsIRDFService | A global RDF service. It is used to generate resource objects that can uniquely identify a resource within an RDF data source. |
| nsIRDFDataSource | An RDF datasource, either a built-in one or one from an RDF file. Methods allow you to get and set values. |
| nsIRDFContainer | A container node within an RDF data source. Methods allow you to add and remove resources. |
| nsIRDFContainerUtils | This interface has some handy container methods to create Seq, Bag and Alt resources. |
In the find files dialog, we could implement the ability to store the most recently searched for items. The search textbox could be replaced by an editable drop-down that contains a list of items that were recently searched for. We will add this capability now.
This will really only work if the dialog has access to a location on disk where the recent search items list can be stored. The most likely places for this are the user's profile directory or a directory the user chooses themselves. Although we won't do that here, the user's profile directory can be retrieved using the '@mozilla.org/file/directory_service' component. To simplify the example, we'll just put a file path directly in the XUL in a datasources attribute.
We could store the recent searches list in a plain text file. However, we can use RDF which already has the ability to read and write its data and update a widget generated from a template automatically. First, the changes to the XUL file. We'll replace the textbox with an editable drop-down list. Replace the value of the datasources attribute with a suitable path. (The file should exist already).
<menulist id="find-text" flex="1" style="min-width: 15em;"
editable="true"
datasources="file:///mozilla/recents.rdf"
ref="urn:findfile:recent">
<template>
<menupopup>
<menuitem label="rdf:http://www.example.com/recent#Label" uri="rdf:*"/>
</menupopup>
</template>
</menulist>All XUL elements that have their children generated by a template have a database property that refers to a nsIRDFDataSource object. This object can then be used to read from and modify the data source used. The database property is placed on the element that has the datasources attribute. This will typically be a tree or, as is the case here, a menulist element.
The database property contains a list (actually an nsISimpleEnumerator) of each of the datasources that were specified in the datasources attribute. That means that we need to iterate over each element, even if there is only one. The following example shows how to do this, assuming only one datasource exists:
var dsource;
var menulist=document.getElementById("find-text");
var sources=menulist.database.GetDataSources();
if (sources.hasMoreElements()){
dsource=sources.getNext();
}
dsource=dsource.QueryInterface(Components.interfaces.nsIRDFDataSource);First, we get a reference to a menulist, which here has an id of find-text. Next we get the list of datasources from the menulist. The nsISimpleEnumerator interface has two methods (it is similar to Java's Enumeration interface). We loop through the elements in the enumeration and, because we assume there is only one, we'll just get it with the getNext method. Finally, we'll call QueryInterface to ensure that it is an nsIRDFDataSource.
We'll use code similar to this to create the recent searches list. First, however, let's initialize the components that we want to use. We'll need three components. The interface nsIRDFService will be used to create resource objects. The interface nsIRDFContainer will be used to add resources to the data source. The third interface, nsIRDFContainerUtils will be used only when the recent searches list is first used, to create the root node. In a script file (findfile.js), add the following code to the top of it. This will be executed when the find files dialog is loaded.
var RDFC = '@mozilla.org/rdf/container;1'; RDFC = Components.classes[RDFC].createInstance(); RDFC = RDFC.QueryInterface(Components.interfaces.nsIRDFContainer); var RDFCUtils = '@mozilla.org/rdf/container-utils;1'; RDFCUtils = Components.classes[RDFCUtils].getService(); RDFCUtils = RDFCUtils.QueryInterface(Components.interfaces.nsIRDFContainerUtils); var RDF = '@mozilla.org/rdf/rdf-service;1' RDF = Components.classes[RDF].getService(); RDF = RDF.QueryInterface(Components.interfaces.nsIRDFService);
This code will create the three services that we need to use. The syntax is similar to other XPCOM object creation code. The first three lines get a reference to an nsIRDFContainer object. Next, we perform a similar operation to get the nsIRDFContainerUtils object. Finally, we repeat again for the nsIRDFService.
Next, we create an initialize function, which we'll call in the onload handler of the window. It will be executed when the window is displayed. Within this code, we'll add code to initialize the RDF objects we created above.
findfile.xul:
<window onload="initSearchList()" ... >
findfile.js:
var dsource;
function initSearchList()
{
var recentlist=document.getElementById("find-text");
var sources=recentlist.database.GetDataSources();
var rootnode=RDF.GetResource("urn:findfile:recent");
while (sources.hasMoreElements()){
try {
dsource=sources.getNext();
dsource=dsource.QueryInterface(Components.interfaces.nsIRDFDataSource);
RDFC.Init(dsource,rootnode);
} catch (e) {
RDFCUtils.MakeSeq(dsource,rootnode);
RDFC.Init(dsource,rootnode);
}
}
}Let's break down the initSearchList function:
var recentlist=document.getElementById("find-text");
var sources=recentlist.database.GetDataSources();
First, we get a reference to the menulist
element that has the datasource on it. It has a
database property that holds the datasources
that are present. We get a reference to the available datasources
and assign it to the variable sources.var rootnode=RDF.GetResource("urn:findfile:recent");
A resource object is generated with the given URI. This will be the
root resource and will be an RDF Seq element
that holds a list of resources, one for each item in the recent searches
list. This function does not get anything from the datasource, it only
converts the URI into a resource identifier. Instead of hard-coding the
URI, we could also get it from the ref
attribute.while (sources.hasMoreElements()){
try {
dsource=sources.getNext();
dsource=dsource.QueryInterface(Components.interfaces.nsIRDFDataSource);
Next, we loop over each datasource to get the right one.RDFC.Init(dsource,rootnode);This function initializes the RDF Container (the nsIRDFContainer interface) with the datasource and the root node. Later, we can use the container object to add new resources inside the container. We'll need to do this to add a searched item to the datasource. An error will occur if the datasource or root node does not exist (for example, if the RDF file was not found). The code was put into a try-catch block to catch the error.
} catch (e){
RDFCUtils.MakeSeq(dsource,rootnode);
RDFC.Init(dsource,rootnode);
}
If an error occured, this is most likely because the root node did not
exist. To create it, we call the method MakeSeq of the nsIRDFContainerUtils
interface. Similar functions exist for creating bags and alts. (MakeBag and
MakeAlt). We then try initializing the container again.The interface nsIRDFService contains a method GetResource that creates a resource object for us, from the string passed in as an argument. This method does not get the value of anything, it simply converts a string into a resource object that can be used to get the value from the datasource. The RDF interfaces do not use strings but instead use resources to refer to things. The value returned by GetResource is of the type nsIRDFResource.
Now that the objects have been initialized, we can add and remove information from the datasource. There are two methods needed depending on whether you want to add a resource to a container or add an assertion from one resource to another. These two cases correspond to adding a bookmark and adding a property such as the title or URL to a bookmark.
We'll add a new entry to the searched for items list when the user clicks the Find button. We'll oversimplify it a bit in several ways. For one, we won't bother checking for duplicate entries. Second, we won't concern ourselves about limiting the length of the list.
Let's add another function that is called from within the doFind function.
function doFind()
{
var recentlist=document.getElementById("find-text");
var fldval=recentlist.value;
addSearchedItem(fldval);
.
.
.This code gets the value entered into the menulist's textbox. We pass the text to the function addSearchedItem which will be defined next.
function addSearchedItem(txt)
{
var newnode=RDF.GetResource("urn:findfile:recent:item"+(RDFC.GetCount()+1));
var labelprop=RDF.GetResource("http://www.example.com/recent#Label");
var newvalue=RDF.GetLiteral(txt);
dsource.Assert(newnode,labelprop,newvalue,true);
RDFC.InsertElementAt(newnode,1,true);
dsource.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush();
} This code does three things, it adds a new resource, it adds a new assertion which holds the value, and then it writes out the modified datasource. Let's break down the code:
var newnode=RDF.GetResource("urn:findfile:recent:item"+(RDFC.GetCount()+1));
This line creates a resource object for the resource we'll be adding.
The function GetCount returns a count of the number of resources that
exist in the container already. This allows us to generate a unique
URI. We could also call GetAnonymousResource (instead of GetResource)
which takes no parameters and generates a random unique URI.var labelprop=RDF.GetResource("http://www.example.com/recent#Label");
We'll be setting the Label property of the resource to the text that was
recently searched for. You could use any property name (and URL) as long as
it's consistent. You'll notice that it has the same value as the
label attribute of the
menuitem element added to the XUL earlier.var newvalue=RDF.GetLiteral(txt);The GetLiteral function generates a RDF string object that will hold the text the user searched for, which was passed in through the txt argument. We don't use GetResource here because we are assigning a value to a resource.
dsource.Assert(newnode,labelprop,newvalue,true);This line will add an assertion to the RDF datasource. In this case, it says that the 'Label' of the resource 'urn:findfile:recent:itemX' is the literal object that was created in the previous line, where X is the number returned from the GetCount function. However, this is only half of what needs to be done. We still need to say that the resource is one of the recent items.
RDFC.InsertElementAt(newnode,1,true);This line adds the resource to the container. Here, we insert it at position 1. (Not that the first element is 1 and not 0.) We could insert it anywhere, or call AppendElement instead to add it to the end. The menulist template will now detect the new resource, and will have an extra row in the list.
dsource.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush();Finally, we write the datasource to disk using the Flush function. This function is not a part of the nsIRDFDataSource interface, so we have to call QueryInterface to convert the datasource into the right interface, nsIRDFRemoteDataSource, first.
If you open the find files dialog now and enter some text, and press Find, you will find that the text appears as one of the choices in the drop-down. Even if you exit and reload, the text will remain in the drop-down.
In order to check for duplicate entries, we could check the existing resources, by using the functions hasAssertion or GetAllResources of the interface nsIRDFDataSource.
This section provides information about cutting, copying and pasting to and from the clipboard.
Mozilla provides a number of interfaces for accessing the clipboard. The component '@mozilla.org/widget/clipboardhelper;1' can be used to copy text to the clipboard. This component implements the interface nsIClipboardHelper, which has a function copyString which can be used to copy a string.
const gClipboardHelper = Components.classes["@mozilla.org/widget/clipboardhelper;1"]
.getService(Components.interfaces.nsIClipboardHelper);
gClipboardHelper.copyString("Put me on the clipboard, please.");This example will first create a clipboard helper and then copy a short string to the clipboard. This method only works to cut strings to the clipboard. For other types of data, such as URLs or images, you will need to use a more complex method.
A component '@mozilla.org/widget/clipboard;1' and an interface nsIClipboard provide general access to the system clipboard. You can use it to copy and paste any type of data from your application to the clipboard. Three XPCOM objects are needed to handle clipboard operations. The first is an object that holds the data to put on the clipboard. The second is the clipboard object. The third is an object which is used to transfer the data from the first object to the clipboard. The clipboard model in Mozilla requires you to perform the following steps to copy data:
You might wonder why a transferring object is needed instead of just putting the object directly on the clipboard. One reason is that some systems do not copy the data right away. Instead, they wait until later or until the data is pasted. Another reason is that the transferable can hold multiple representations of the same data. For example, a piece of HTML can be represented in both its original HTML form and in plain text. If an application wants to get the data from the clipboard and doesn't understand HTML, it can use the plain text version. If it does understand HTML, it can grab that version. The transferring object will hold the clipboard contents until the application has decided what it needs. This allows the clipboard to be used by another application right away.
Let's break down the steps needed to copy data to the clipboard. First, we need to create an XPCOM object to wrap what we want to copy. We'll assume that we want to copy some text. We will use the interface nsISupportsWString which can be used to represent strings (specifically, Unicode strings).
var copytext="Text to copy"; var str = Components.classes["@mozilla.org/supports-wstring;1"].createInstance(Components.interfaces.nsISupportsWString); str.data=copytext;
The first line holds the text that we want to copy. Next, the variable 'str' is assigned to a component that can be used to hold a string. The third line assigns the string to the component using the data property. Here, the string "Text to copy" will be copied but you can replace this with the text string that you want to copy. Now that we have the object to copy, a transferring object needs to be created:
var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
trans.addDataFlavor("text/unicode");
trans.setTransferData("text/unicode",str,copytext.length*2);The first line gets the transferring component which implements nsITransferable. Next, we need to tell the transferable what type of data we want to use. The type of data is referred to as a data flavor. The function addDataFlavor is used to tell the transferable that it needs to transfer data of a certain flavour. In this case, we are transferring the flavor "text/unicode" which is a Unicode string. Then, the function setTransferData is called which copies the data from the string into the transferable. This function takes three parameters. The first is the flavor we are setting, the second is the object holding the string and the third is the length of the data, in bytes. Here, the length is multiplied by two because we are using a Unicode string which requires two bytes per character.
You can repeat the last two lines and call addDataFlavor and setTransferData for multiple flavors. That way, you could have a text version and an HTML version of the content. The Transferable object will hold its own copy of the data. When you've added all the flavors you want, you can put it all on the clipboard at once. The transferable object will hold all of the data that you want until you're ready to put it on the clipboard.
Next, we need to create a clipboard object that refers to the system clipboard.
var clipid=Components.interfaces.nsIClipboard; var clip = Components.classes["@mozilla.org/widget/clipboard;1"].getService(clipid); clip.setData(trans,null,clipid.kGlobalClipboard);
We get the system clipboard object and store it in the clip variable. We can copy the data to the clipboard by calling the function setData. The first parameter of this function is the transferable. The second parameter can usually be set to null but you could set it to a nsIClipboardOwner so that you can tell when the data you've copied is overwritten by another copy operation. Call setData only when you're ready to copy to the system clipboard.
The third parameter to setData (and the parameter to emptyClipboard) indicates which clipboard buffer to use. The above code uses the constant kGlobalConstant for this, which refers to the global clipboard. This would be the same one that cut and paste operations from the Edit menu typically use. If you use kSelectionClipboard instead, you will copy into the selection buffer, which is generally only available on Unix systems.
This multi-step process has resulted in text being copied on the clipboard. We can cut to the clipboard instead of copying by doing a copy and then deleting the original data. Normally, the text would be in a document or textbox. The code is put together below, with additional error checking:
var copytext="Text to copy";
var str = Components.classes["@mozilla.org/supports-wstring;1"].createInstance(Components.interfaces.nsISupportsWString);
if (!str) return false;
str.data=copytext;
var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
if (!trans) return false;
trans.addDataFlavor("text/unicode");
trans.setTransferData("text/unicode",str,copytext.length*2);
var clipid=Components.interfaces.nsIClipboard;
var clip = Components.classes["@mozilla.org/widget/clipboard;1"].getService(clipid);
if (!clip) return false;
clip.setData(trans,null,clipid.kGlobalClipboard);To paste data from the clipboard we can use a similar process, except we use getData instead of setData and getTransferData instead of setTransferData. Here are the steps to pasting:
The first steps are similar to that used for copying:
var clip = Components.classes["@mozilla.org/widget/clipboard;1"].createInstance(Components.interfaces.nsIClipboard);
if (!clip) return false;
var trans = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
if (!trans) return false;
trans.addDataFlavor("text/unicode");This code gets the system clipboard object and a transferable object. The flavor is added to the transferable. Next, we need to get the data from the clipboard:
clip.getData(trans,clip.kGlobalClipboard);
var str=new Object();
var strLength=new Object();
trans.getTransferData("text/unicode",str,strLength);The first line performs the opposite of getData. The data currently on the system clipboard is placed into the transferable. Next we create two JavaScript objects which will hold the data and the length of the data. Note that we have no idea what type of data is currently on the clipboard. It may have been placed there by another application. This is why we use generic Objects for str and strLength.
Then we use getTransferData to retrieve the data from the transferable. We specify the flavor we would like to get. The data will be converted if it is not of the desired flavor and a conversion between the actual and desired flavor is possible. If you originally copied data of multiple flavors onto the clipboard, you can retrieve the data in the best format necessary. For example, a textbox would accept text/unicode (or text/plain) while a Composer window might accept HTML and image data.
The variable str now holds the data from the clipboard. We need to convert the data back into a JavaScript string from an XPCOM object. The code below can be used for this purpose:
if (str) str=str.value.QueryInterface(Components.interfaces.nsISupportsWString); if (str) pastetext=str.data.substring(0,strLength.value / 2);
Because the data from the transferable is a nsISupportsWString, we need to convert it into a JavaScript string. The property value of the object filled in by getTransferData, which is str in this case, provides the actual value of the object.
We assign the string to the variable pastetext. We can then put that into a textbox or other location as necessary.
This section describes how to implement objects that can be dragged around and dropped onto other objects.
Many user interfaces allow one to drag particular objects around within the interface. For example, dragging files to other directories, or dragging an icon to another window to open the document it refers to. Mozilla and XUL provide a number of events that can handle when the user attempts to drag objects around.
A user can start dragging by holding down the mouse button and moving the mouse. The drag stops when the user releases the mouse. Event handlers are called when the user starts and ends dragging, and at various points in-between.
Mozilla implements dragging by using a drag session. When a user requests to drag something that can be dragged, a drag session should be started. The drag session handles updating the mouse cursor and where the object should be dropped. If something cannot be dragged, it should not start a drag session. Because the user generally has only one mouse, only one drag session is in use at a time.
Note that drag sessions can be created from within Mozilla itself or from other applications. Mozilla will translate the data being dragged as needed.
The list below describes the event handlers that can be called, which may be placed on any element. You only need to put values for the handlers where you need to do something when the event occurs.
There are two ways that drag and drop events can be handled. This first invloves using the drag and drop XPCOM interfaces directly. The second is to use a JavaScript wrapper object that handles some of this for you. This wrapper is contained in the widget-toolkit (or global) package.
Two interfaces are used to support drag and drop. The first is a drag service, nsIDragService and the second is the drag session, nsIDragSession.
The nsIDragService is responsible for creating drag sessions when a drag starts, and removing the drag session when the drag is complete. The function 'invokeDragSession' should be called to start a drag inside an ondraggesture event handler. Once this function is called, a drag has started.
The function invokeDragSession takes four parameters, as described below:
invokeDragSession(element,transferableArray,region,actions);
The interface nsIDragService also provides the function 'getCurrentSession' which can be called from within the drag event handlers to get and modify the state of the drag. The function returns an object that implements nsIDragSession.
The interface nsIDragSession is used to get and set properties of the drag that is currently occuring. The following properties and methods are available:
This section describes how to use the JavaScript wrapper for drag and drop.
The JavaScript wrapper to drag and drop simplifies the process by handling all of the XPCOM interfaces for you. It works by providing an object which implements the event handlers. All you have to do is write some simpler functions which work with the data being dragged.
This drag and drop interface is stored in the global package, in the file 'chrome://global/content/nsDragAndDrop.js'. You can include this file in your XUL file with the script tag in the same way you would include your own scripts. The library also depends on another script libraries, which you should also include, usually at the top of your XUL file. You can look at the contents of these files to see how drag and drop is done at a lower level.
Note that you can only use these libraries from within XUL loaded via a chrome URL.
<script src="chrome://global/content/nsDragAndDrop.js"/> <script src="chrome://global/content/nsTransferable.js"/>
This drag and drop library creates an object stored in the variable nsDragAndDrop. The object contains a series of functions, one for each event handler (except for dragenter where it has nothing special to do). Each of the functions takes two parameters, the first is the event object and the second is an observer object that you create. More on that in a moment.
The following is an example of calling the nsDragAndDrop object.
<button label="Drag Me" ondraggesture="nsDragAndDrop.startDrag(event,buttonObserver);
The function 'startDrag' will be called when a drag starts on the button. The first parameter is the event object, available in all event handlers. The second parameter to this function is the observer, which we'll create soon. In this case we only do anything special when the button drag is started. If we wanted to handle the other cases also, we can call the other functions, as in the next example:
<description value="Click and drag this text."
ondraggesture="nsDragAndDrop.startDrag(event,textObserver)"
ondragover="nsDragAndDrop.dragOver(event,textObserver)"
ondragexit="nsDragAndDrop.dragExit(event,textObserver)"
ondragdrop="nsDragAndDrop.drop(event,textObserver)">As mentioned earlier, there is nothing special that happens during a dragenter event, so you can just write that yourself.
The functions are implemented by the nsDragAndDrop object, which is declared in the file nsDragAndDrop.js, which was included in one of the script tags. They handle the event, handle the XPCOM interfaces and pass a simpler data structure to functions of the observer object.
The observer is an object that you declare yourself. In the above examples, this observer is stored in the buttonObserver and textObserver variables. The observer is declared in a script which you would include in the XUL file using the script tag. The observer is an object which may have a number of properties, each set to a function which handles a particular aspect of drag and drop. Five functions may be defined. You only have to define the ones that you need.
For an observer that is observing an element that can start a drag, you should define at least the onDragStart function. For elements that can have objects dropped on them, you should define onDragOver, onDrop and getSupportedFlavours (and, if desired, onDragExit).
The type of data being dragged is stored as a set of flavours. Often, a dragged object will be available in a number of flavours. That way, a drop target can accept the flavour it finds most suitable. For example, a file may come in two flavours, the file itself and the text name of the file. If the file is dragged and dropped onto a directory, the file flavour will be used. If the file is dropped onto a textbox, the text name flavour will be used. The text is therefore used to insert the name of the file when files can't be dropped directly.
A flavour object has a name, which is a formatted like a MIME type, such as 'text/unicode'. Within the onDragStart function, you specify what flavours are available for the item being dragged. To do this, add data and flavours to the transferData object, which is the second argument to onDragStart.
An example should help here. The onDragStart function adds data to the transferData object.
var textObserver = {
onDragStart: function (evt , transferData, action){
var htmlText="<strong>Cabbage</strong>";
var plainText="Cabbage";
transferData.data=new TransferData();
transferData.data.addDataForFlavour("text/html",htmlText);
transferData.data.addDataForFlavour("text/unicode",plainText);
},Here, an observer has been declared and stored in the variable 'textObserver'. It has one property called onDragStart. (In JavaScript, properties can be declared with the syntax name : value). This property is a function which sets the data that is being dragged.
Once called, it starts a drag for the string data "Cabbage". Of course, you would want to calculate this value from the element that was clicked on. Conveniently, this element is available from the event object's target property. The event object is passed as the first argument to onDragStart.
We create a TransferData object which can be used to hold all the data to be dragged. We add two pieces of data to the transfer data. The first is a string of HTML text and the second is a string of plain text. If the user drops onto an area which can accept HTML (such as Mozilla's editor window), the HTML flavour will be used and the text will appear bold. Otherwise, the plain text version will be used instead.
Usually you will want to provide a text version of the data so that more applications can accept the data. The order that you define the flavours should be from the best match to the weakest match. In this case above, the HTML flavour (text/html) comes first and then the text flavour (text/unicode).
The example below shows how to set the data to be dragged from the element's label attribute. In this case we only provide the data in one flavour.
var textObserver = {
onDragStart: function (evt){
var txt=evt.target.getAttribute("label");
transferData.data=new TransferData();
transferData.data.addDataForFlavour("text/unicode",txt);
},This might be useful when implementing drag and drop for cells in a tree. You can use the value of a cell, or some resource from an RDF file if the tree is built from a template, as the value of a drag. If you store it as a string, any object that accepts strings dragged onto it can grab the dragged data.
You will need to add an observer to each element that can either start a drag action or can accept dropped objects. You can reuse the same observer for multiple elements. For an element that can start a drag, onStartDrag is all that is necessary to implement.
For an element that can be dropped on, the observer will need to implement at least the getSupportedFlavours, onDragOver and onDrop functions. Some elements may be able to initiate a drag and accept a drop. In this case, the onStartDrag function will be necessary as well.
The getSupportedFlavours function should return a list of flavours that the element being dragged over can accept for dropping. A file system directory view might accept files and perhaps text, but wouldn't accept HTML text. Below, we'll define a getSupportedFlavours function. We'll allow only one flavour here, that for a string.
var textObserver = {
getSupportedFlavours : function () {
var flavours = new FlavourSet();
flavours.appendFlavour("text/unicode");
return flavours;
}The flavours list contains only one flavour, which is 'text/unicode'. The FlavourSet object can be used to hold a list of flavours. In some cases, you must provide the XPCOM interface as well. For example, for files:
var textObserver = {
getSupportedFlavours : function () {
var flavours = new FlavourSet();
flavours.appendFlavour("application/x-moz-file","nsIFile");
flavours.appendFlavour("text/unicode");
return flavours;
}The onDragOver function defines what happens when an object is dragged over. You might use it to change the appearance of the element as it is being dragged over. In many cases the function can do nothing. It must be defined for elements that accept dragged data however.
Next, the onDrop function should be created. Its second argument is the transfer data object that holds the data being dragged. By the time onDrop is called, the wrapper has called getSupportedFlavours to determine the best flavour for the drop, so the transfer object only contains the data for the best matching flavour.
The transfer object has two properties, 'data' which holds the data and 'flavour' which holds the flavour of the data. Once you have the data, you can add it to the element is some way. For example, you might set the value of a textbox.
var textObserver = {
onDrop : function (evt, transferData, session) {
event.target.setAttribute("value",transferData.data);
}The flavour system used allows multiple objects of various types to be dragged at once and also allows alternative forms of the data to be dragged. The following table describes some of the flavours you might use. You can also make up your own flavours if necessary.
| text/unicode | Text data |
| text/html | HTML data |
| application/x-moz-url | A URL |
| application/x-moz-file | A local file |
An example of implementing drag and drop will be implemented in this section.
Here, we'll create a simple board where items from a palette can be dragged onto the board. The user can click on one of several XUL elements on the palette and drag it onto a stack element to create an element of a particular type.
First, we'll add the wrapper scripts:
<script src="chrome://global/content/nsDragAndDrop.js"/> <script src="chrome://global/content/nsTransferable.js"/> <script src="dragboard.js"/>
An additional script file dragboard.js is included which will contain the code we will write ourselves.
The board will be created using a stack element. We'll use some style properties to set the width and height of the stack. A maximum size is also specified so that it doesn't resize when new elements are dragged onto it.
The board will need to respond to the dragdrop event so that an element is created when the user drags onto it.
<stack id="board"
style="width:300px; height: 300px; max-width: 300px; max-height: 300px"
ondragover="nsDragAndDrop.dragOver(event,boardObserver)"
ondragdrop="nsDragAndDrop.drop(event,boardObserver)">
</stack>The board only needs to respond to the dragdrop and dragover events. We'll add a boardObserver to the file dragboard.js in a moment.
Next, a palette will be added to the right side of the window. It will contain three buttons, one to create new buttons, one to create check boxes and the other to create textboxes. This buttons will respond to the draggesture event and start a drag.
<vbox>
<button label="Button"
elem="button" ondraggesture="nsDragAndDrop.startDrag(event,listObserver)"/>
<button label="Check Box"
elem="checkbox" ondraggesture="nsDragAndDrop.startDrag(event,listObserver)"/>
<button label="Text Box"
elem="textbox" ondraggesture="nsDragAndDrop.startDrag(event,listObserver)"/>
</vbox>The nsDragAndDrop object will be called to do most of the work. We'll create a listObserver object that will set the data to be dragged. Note that each button here has an additional elem attribute. The is a made-up attribute. XUL doesn't handle it and just ignores it, but we can still retrieve it with the DOM's getAttribute function. We need this so that we know what type of element to create when dragging.
Next, we'll define the two listener objects. First, the listObserver which needs a function to handle the start of the drag.
var listObserver = {
onDragStart: function (evt,transferData,action){
var txt=evt.target.getAttribute("elem");
transferData.data=new TransferData();
transferData.data.addDataForFlavour("text/unicode",txt);
}
};One function has been defined, onDragStart, which will be called by the nsDragAndDrop object when necessary. The function adds the data to be dragged to the transfer object. The elem attribute is retrieved from the target of the drag event. The target will be the element that had the drag start on. We'll use the value of this attribute as the data of the drag.
The boardObserver will need three functions, getSupportedFlavours, onDragOver and onDrop. The onDrop function will grab the data from the drag session and create a new element of the appropriate type.
var boardObserver = {
getSupportedFlavours : function () {
var flavours = new FlavourSet();
flavours.appendFlavour("text/unicode");
return flavours;
},
onDragOver: function (evt,flavour,session){},
onDrop: function (evt,dropdata,session){
if (dropdata.data!=""){
var elem=document.createElement(dropdata.data);
evt.target.appendChild(elem);
elem.setAttribute("left",""+evt.pageX);
elem.setAttribute("top",""+evt.pageY);
elem.setAttribute("label",dropdata.data);
}
}
};The getSupportedFlavours function needs only to return a list of flavours that the stack can accept to be dropped on it. In this case, it only accepts text. We don't need to do anything special for the onDragOver function, so no code is added in its body.
The onDrop handler first uses the createElement function to create a new element of the type stored in the drag session. Next, appendChild is called to add the new element to the stack, which is the target of the event. Finally, we set some attributes on the new element.
The position of elements in a stack is determined by the left and top attributes. The values of the pageX and pageY properties store the mouse pointer coordinates on the window where the drop occured. This allows us to place the new element at the position where the mouse button was released. This isn't quite the correct way to do this as we actually need to calculate the coordinates of the event relative to the stack. It works here because the board is at the top-left corner of the window.
The label attribute is set to the data from the drag also so that the button has a default label.
This example is fairly simple. One possible change is to use a custom flavour for the data instead of text. The problem with using text is that if the text from an external drag just happens to be set to 'button', a button will be created on the board. A custom type means that the board will only accept drags from the palette.
The final code is shown below:
Example 8.7.1: Source<window title="Widget Dragger" id="test-window"
orient="horizontal"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script src="chrome://global/content/nsDragAndDrop.js"/>
<script src="chrome://global/content/nsTransferable.js"/>
<script src="dragboard.js"/>
<stack id="board"
style="width:300px; height: 300px; max-width: 300px; max-height: 300px"
ondragover="nsDragAndDrop.dragOver(event,boardObserver)"
ondragdrop="nsDragAndDrop.drop(event,boardObserver)">
</stack>
<vbox>
<button label="Button"
elem="button" ondraggesture="nsDragAndDrop.startDrag(event,listObserver)"/>
<button label="Check Box"
elem="checkbox" ondraggesture="nsDragAndDrop.startDrag(event,listObserver)"/>
<button label="Text Box"
elem="textbox" ondraggesture="nsDragAndDrop.startDrag(event,listObserver)"/>
</vbox>
</window>var listObserver = {
onDragStart: function (evt,transferData,action){
var txt=evt.target.getAttribute("elem");
transferData.data=new TransferData();
transferData.data.addDataForFlavour("text/unicode",txt);
}
};
var boardObserver = {
getSupportedFlavours : function () {
var flavours = new FlavourSet();
flavours.appendFlavour("text/unicode");
return flavours;
},
onDragOver: function (evt,flavour,session){},
onDrop: function (evt,dropdata,session){
if (dropdata.data!=""){
var elem=document.createElement(dropdata.data);
evt.target.appendChild(elem);
elem.setAttribute("left",""+evt.pageX);
elem.setAttribute("top",""+evt.pageY);
elem.setAttribute("label",dropdata.data);
}
}
};We have hardly modified the look of the elements we have created so far. XUL uses CSS (Cascading Style Sheets) to customize elements.
A style sheet is a file which contains style information for elements. It was originally designed for HTML elements but can be applied to XUL elements also, or to any XML for that matter. The style sheet contains information such as the fonts, colors, borders, and size of elements.
Mozilla applies a default style sheet to each XUL window. In many cases, it will suffice to leave the defaults as is. Other times, however, you will want to provide a custom style sheet. In general, you will associate a single style sheet with each XUL file.
You can place a style sheet anywhere you wish. If your XUL file is stored remotely and accessed via an HTTP URL, you can store the style sheet remotely as well. If you are creating a XUL package to be installed as part of the chrome system, you have two choices. First, you could store the style sheet in the same directory as the XUL file. This method has the disadvantage because it means your application will not be themeable. The second method involves placing your files as part of a theme.
Let's assume that we are building the find files dialog for themeability, because the find files dialog can be referred to with the URL chrome://findfile/content/findfile.xul so the style sheet file will be stored in chrome://findfile/skin/findfile.css.
All the XUL examples so far have actually been using a style sheet already. The second line has always been:
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
This line indicates that we want to use the style provided by chrome://global/skin/. In Mozilla, this will be translated as the file global.css, which contains default style information for XUL elements. You could leave the line out and the elements will still work, however they will look fairly plain. The style sheet applies various fonts, colors and borders to make the elements look more suitable.
However, there will be times when the default look of elements will not give the look that is desired. For this, we will need to add a style sheet of our own. So far, we have been applying styles using the style attribute on elements. Although this works, it is not really the best thing to do. It is much better to create a separate style sheet. The reason is so that different looks, or skins, can be applied easily.
There may be certain cases where the style attribute is acceptable. An example would be when a script changes the style, or where a difference in layout might change the meaning of the element. However you should avoid this as much as possible.
For installed files, you'll have to create or modify a manifest file and install the skin.
Let's modify the find files dialog so that its style comes from a separate style file. First, the modifed lines of findfile.xul:
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <?xml-stylesheet href="findfile.css" type="text/css"?> ... <spacer class="titlespace"/> <groupbox orient="horizontal"> <caption label="Search Criteria"/> <menulist id="searchtype"> <menupopup> <menuitem label="Name"/> <menuitem label="Size"/> <menuitem label="Date Modified"/> </menupopup> </menulist> <spacer class="springspace"/> <menulist id="searchmode"> <menupopup> <menuitem label="Is"/> <menuitem label="Is Not"/> </menupopup> </menulist> <spacer class="springspace"/> <menulist id="find-text" flex="1" editable="true" datasources="file:///mozilla/recents.rdf" ref="urn:findfile:recent"/> ... <spacer class="titlespace"/> <hbox> <progressmeter id="progmeter" value="50%" style="display:none;"/>
The new xml-stylesheet line is used to import the style sheet. It will contain the styles instead of having them directly in the XUL file. You can include any number of style sheets in a similar way. Here the style sheet is placed in the same directory as findfile.xul.
Some of the styles in the code above have been removed. One that wasn't was the display property on the progressmeter. This will be changed by a script so it was left in, as it doesn't really make sense to have the progress bar visible initially. You could still put this in a separate style sheet if you really wanted to. A class was added to the spacers so that they can be referred to.
A style sheet also needs to be created. Create a file findfile.css in the same directory as the XUL file. (It would normally be put into a separate skin). In this file, we'll add the style declarations, as shown below:
#find-text {
min-width: 15em;
}
#progmeter {
margin: 4px;
}
.springspace {
width: 10px;
}
.titlespace {
height: 10px;
}
Notice how these styles are equivalent to the styles we had before. However, it is much easier for someone to change the look of the find files dialog now because they could add or modify the style declarations by either modifying the file or by changing the skin. If the user changes the interface skin, the files in a directory other than default will be applied.
We've already seen how to import style sheets for use. An example is shown below:
<?xml-stylesheet href="chrome://bookmarks/skin/" type="text/css"?>
This might be the first lines of a bookmarks window. It imports the bookmarks style sheet, which is bookmarks.css. Mozilla's skin system is smart enough to figure out which style sheet to use, because the specific filename was not indicated here. We have done a similar thing with the global style sheet file (chrome://global/skin).
A style sheet may import styles from another stylesheet using the import directive. Normally, you will only import one style sheet from each XUL file. The global style sheet can be imported from within the style sheet associated with the XUL file. This can be done with the code below, allowing you to remove the import from the XUL file:
Style import from XUL: <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> Style import from CSS: @import url(chrome://global/skin/);
The second syntax is preferred because it reduces the number of dependencies within the XUL file itself.
Remove the global style sheet import from findfile.xul and add the import to findfile.css.
All elements can be styled using CSS. You can use selectors to select the element that you wish to have styled. (The selector is the part before the curly brace in a style rule). The following table summarizes some of the selectors available:
| button | Matches all button tags |
| #special-button | Matches the element with an id of special-button |
| .bigbuttons | Matches all elements with a class of bigbuttons |
| button.bigbuttons | Matches all button elements with a class of bigbuttons |
| toolbar > button | Matches all buttons that are directly inside toolbar elements. |
| toolbar > button.bigbuttons | Matches all button elements with a class of bigbuttons that are directly inside toolbar elements. |
| button.bugbuttons:hover | Matches all button elements with a class of bigbuttons but only while the mouse is over them. |
| button#special-button:active | Matches all button elements with an id of special-button but only while they are active (being clicked on). |
| box[orient="horizontal"] | Matches all box elements that have an orient attribute that is set to horizontal. |
You can combine these rules in any way that you wish. It is always a good idea to be as precise as possible when specifying what gets styled. It is more efficient and it also reduces the likelihood that you'll style the wrong thing.
The following describes how to style a tree.
You can style a tree and the column headers in the same way as other elements. Style added to the tree element will apply to the entire tree. Adding a style to the treecol element does not cause the style to be applied to the column but only to the header.
The body of the tree must be styled in a somewhat different way than other elements. This is because the tree body is stored in a different way to other elements. The outer treechildren is the only real element in the tree body. The inner elements are just placeholders.
Instead, you must use the properties attribute on the rows or cells to set one or more named properties. This can be used with trees with static content, RDF built content or with those with a custom view. Let's say we want to give a particular row have a blue background color. This would be used to implement Mozilla Mail's labels feature. We'll use a property called 'makeItBlue'. You can use whatever name you want. You can set multiple properties by separating them with spaces.
Set the property on a row or cell, as in the following example:
<treerow properties="makeItBlue">
The style sheet can take this property and use it to change the appearance of the row for unread messages or labels. You can think of the properties as functioning much like style classes, although they require a somewhat more complex syntax to use in a style sheet. This is because you can specify the style for a number of parts of the cell individually. You can style not only the cell and its text, but the twisty and indentation. The following is the syntax that needs to be used:
treechildren:-moz-tree-row(makeItBlue)
{
background-color: blue;
}This extra pseudostyle is used to style the background color of rows that have the 'makeItBlue' property. This special syntax is needed because the cells themselves are not separate elements. All of the content inside the tree's body is rendered by the treechildren element. (Note the treechildren is being styled in the rule above.) The pseudostyle sets style rules for particular parts of what it displays. This style rule means, inside a treechildren element, set the background color to blue for all tree rows that have the 'makeItBlue' property.
The text ':-moz-tree-row' specifies what content area is desired, which in this case is a row. You can also use the following values:
You can check for multiple properties by separating them with commas. The example below sets the background color to grey for rows that have the 'readonly' and 'unread' properties. For properties that are 'readonly', it adds a red border around the row. Note that the first rule will apply to any row that is 'readonly' regardless of whether other properties such as 'unread' are set.
treechildren:-moz-tree-row(readonly)
{
border: 1px solid red;
}
treechildren:-moz-tree-row(readonly, unread)
{
background-color: rgb(80%, 80%, 80%);
}The properties list for tree elements contain a small number of default properties, which you can also use in a style sheet. You can use these extra properties to set the appearance of containers or selected rows. The following properties are automatically set as needed:
The properties are set for rows or cells in rows with the necessary state. For columns and cells, one additional property, the id of the column or column the cell is in will be set.
For RDF-built trees, you can use the same syntax. However, you will often set the properties based on values in the datasource.
For trees with a custom view script, you can set properties by supplying the functions 'getRowProperties', 'getColumnProperties' and 'getCellProperties' in the view. These return information about an individual row, column and cell. Arguments to these functions indicate which row and/or column. The last argument to each of these functions is a properties list which the view is expected to fill with a list of properties. The function 'getColumnProperties' also supplies the corresponding treecol element for the column.
getRowProperties : function(row,prop){}
getColumnProperties : function(column,columnElement,prop){}
getCellProperties : function(row,column,prop){}Let's look at an example of changing a specific cell. Let's make every fourth row have blue text, using the example from a previous section. We'll need to add code to the getCellProperties function, to add a property 'makeItBlue' for cells in every fourth row. (We don't use getRowProperties as the text color will not be inherited into each cell.)
The properties object that is passed as the last argument to the getCellProperties is an XPCOM object that implements nsISupportsArray. It is really just an XPCOM version of an array. It contains a function AppendElement which can be used to add an element to the array. We can use the interface nsIAtomService to constuct string atoms for the properties.
getCellProperties: function(row,col,props){
if ((row %4) == 0){
var aserv=Components.classes["@mozilla.org/atom-service;1"].
createInstance(Components.interfaces.nsIAtomService);
props.AppendElement(aserv.getAtom("makeItBlue"));
}
}This function would be defined as part of a view object. It first checks to see which row is being requested and sets a property for cells in every fourth row. The properties list requires an array of atom objects, which can be thought of as constant strings. We create them using the XCPOM interface nsIAtomService and add them to the array using the AppendElement function. Here, we create an atom 'makeItBlue'. You can call AppendElement again to add additional properties.
This section describes how to modify the skin of a window.
A skin is a set of style sheets, images and behaviors that are applied to a XUL file. By applying a different skin, you can change the look of a window without changing its functionality. Mozilla provides two skins by default, Classic and Modern, and you may download others. The XUL for both is the same, however the style sheets and images used are different.
For a simple personalized look to a Mozilla window, you can easily change the style sheets associated with it. Larger changes can be done by creating an entirely new skin. Mozilla's preferences window has a panel for changing the default skin.
A skin is described using CSS, allowing you to define the colors, borders and images used to draw elements. The files classic.jar and modern.jar contain the skin definitions. The global directory within these archives contain the main style definitions for how to display the various XUL elements. By changing these files, you can change the look of the XUL applications.
If you place a file called 'userChrome.css' in a directory called 'chrome' inside your user profile directory, you can override settings without changing the archives themselves. This directory should be created when you create a profile and some examples placed there. The file 'userContent.css' customizes Web pages, whereas 'userChrome.css' customizes chrome files.
For example, by adding the following to the end of the file, you can change all menubar elements to have a red background.
menubar {
background-color: red;
}If you open any Mozilla window after making this change, the menu bars will be red. Because this change was made to the user style sheet, it affects all windows. This means that the browser menu bar, the bookmarks menu bar and even the find files menu bar will be red.
To have the change affect only one window, change the style sheet associated with that XUL file. For example, to add a red border around the menu commands in the address box window, add the following to addressbook.css in the modern.jar or classic.jar archive.
menuitem {
border: 1px solid red;
}If you look in one of the skin archives, you will notice that each contain a number of style sheets and a number of images. The style sheets refer to the images. You should avoid putting references to images directly in XUL files if you want your content to be skinnable. This is because a particular skin's design might not use images and it may need some more complex design. By referring to the images with CSS, they are easy to remove. It also removes the reliance on specific image filenames.
You can asign images to a button, checkbox and other elements by using the list-style-image property as in the following:
checkbox {
list-style-image: url("chrome://findfile/skin/images/check-off.jpg");
}
checkbox[checked="true"] {
list-style-image: url("chrome://findfile/skin/images/check-on.jpg");
}This code changes the image associated with a checkbox. The first style sets the image for a normal checkbox and the second style sets the image for a checked checkbox. The modifier 'checked=true' makes the style only apply to elements which have their checked attributes set to true.
This section describes how to create a simple skin. For simplicity, we'll only apply it to the find files dialog.
The image below shows the current find files dialog. Let's create a skin that we can apply to it. Normally, a skin would apply to the enitre application, but we'll focus on just the find files dialog to make it easier. For this reason, we'll modify only the file findfile.css rather than the global.css file. This section assumes that you are starting with the Classic skin. You may wish to make a copy of the files used by the find files dialog before editing.
You need to create a file 'findfile.css' in a custom skin. Or, you can temporarily place it in the content directory and refer to it using a stylesheet directive. You can modify the existing findfile.css directly to see what it looks like, or you can create a custom skin and link to that. To create a skin, do the following:
Copy the original findfile.css into the new directory. We'll use this as a basis for the new skin. We can then refer to it using the URL 'chrome://findfile/skin/findfile.css'. First, let's decide what kind of changes we want to make. We'll make some simple color changes, modify the button styles, and modify the spacing a bit. Let's start with the menus, toolbars and the overall tab pabel.
The following style rules added to findfile.css will cause the changes shown in the accompanying image.
window > box {
background-color: #0088CC;
}
menubar,menupopup,toolbar,tabpanels {
background-color: lightblue;
border-top: 1px solid white;
border-bottom: 1px solid #666666;
border-left: 1px solid white;
border-right: 1px solid #666666;
}
caption {
background-color: lightblue;
}
The inner box of the window (which actually surrounds all of the window content) has been changed to have a medium blue color. You can see this blue behind the tab strip and along the bottom of the window. Four elements, the menubar, the menupopup, the toolbar and the tabpanels appear in light blue. The border around these four elements has been changed to give a heavier 3D appearance. You can see this if you look closely. The background color of the caption has also been changed to match the background.
The first rule above (for 'window > box') specifies that the child box of the window has a different color. This probably isn't the best way to do this. We should really change this to use a style class. Let's do this. That way, we can modify the XUL without needing to keep the box as the first child of the window.
.findfilesbox {
background-color: #0088CC;
}
XUL:
<vbox class="findfilesbox" orient="vertical" flex="100%">
<toolbox>Next, let's modify the tabs. We will make the selected tab bold and change the rounding on the tabs.
tab:first-child {
-moz-border-radius: 4px 0px 0px 0px;
}
tab:last-child {
-moz-border-radius: 0px 4px 0px 0px;
}
tab[selected="true"] {
color: #000066;
font-weight: bold;
text-decoration: underline;
} Two rules change the normal tab appearance, the first sets the rounding on the first tab and the second sets the rounding on the last tab. Used here is a special Mozilla style rule, -moz-border-radius, that creates rounded border corners. The upper left border of the first tab and the upper right border of the second tab are rounded by four pixels and the other corners have a round corner of zero pixels, which is equivalent to no rounding. Increase the values here for more rounding and decrease them for a more rectangular look.
The last rule only applies to tabs that have their
selected attribute set to
true. It makes the text in the selected
tab appear bold, underlined and dark blue. Note in the image that
this style has applied only to the first tab, because it is the selected
one.
It is somewhat difficult to distinguish the buttons on the toolbar from the commands on the menu. We could add some icons to the buttons to make them clearer. Mozilla Composer provides some icons for open and save buttons, which we'll just use here to save time. We can set the image for a button using the list-style-image CSS property.
#opensearch {
list-style-image: url("chrome://editor/skin/icons/btn1.gif");
-moz-image-region: rect(48px 16px 64px 0);
-moz-box-orient: vertical;
}
#savesearch {
list-style-image: url("chrome://editor/skin/icons/btn1.gif");
-moz-image-region: rect(80px 16px 96px 0);
-moz-box-orient: vertical;
}Mozilla provides a custom style property -moz-image-region which can be used to make an element use part of an image. You can think of it as a clip region for the image. You set the property to a position and size within an image and the button will display only that section of the image. This allows you to use the same image for multiple buttons and set a different region for each one. When you have lots of buttons, with states for hover, active and disabled, this saves space that would normally be occupied by mutliple images. In the code above, we use the same image for each button, but set a different image region each one. If you look at this image (btn1.gif), you will notice that it contains a grid of smaller images, each one 16 by 16 pixels.
The -moz-box-orient property is used to orient
the button vertically, so that the image appears above the label. This
property has the same meaning as the orient
attribute. This is convenient because the skin cannot change the XUL. Most
of the box attributes have corresponding CSS properties.
Next, we'll make a couple of changes to the buttons along the bottom, again reusing some icons from Mozilla to save time. If creating your own skin, you will need to create new icons or copy the icons to new files. If following the example in this section, just copy the files to your new skin and change the URLs accordingly.
#find-button {
list-style-image: url("chrome://global/skin/checkbox/images/cbox-check.jpg");
font-weight: bold;
}
#cancel-button {
list-style-image: url("chrome://global/skin/icons/images/close-button.jpg");
}
button:hover {
color: #000066;
}
We add some images to the buttons and make the Find button have bold text
to indicate that it is the default button. The last rule applies to buttons
when the mouse is hovering over them. We set the text color to dark blue
in this case. Finally, some minor changes to the spacing around the items,
by setting margins:
tabbox {
margin: 4px;
}
toolbarbutton {
margin-left: 3px;
margin-right: 3px;
}After those changes, the find files dialog now looks like the following:
As you can see, some simple changes to the style rules has resulted in quite a different appearance to the find files dialog. We could continue by changing the menus, the grippies on the toolbar and the input and checkbox elements.
The skin created above is simple and only applies to the find files dialog. Some of the changes made to the skin could be placed in the global style sheets (those in the global directory of the skin) to be applied to all applications. For example, having different images for the check boxes in the find files dialog as other windows looks a little odd. This change should really be moved into the global style sheet.
Try moving the CSS styles from findfile.css into global.css and then look at some of the dialogs in Mozilla. (The cookie viewer is a good example.) You will notice that it has adopted the rules that we have added. Some of the rules conflict with those already in the global stylesheets. For example, rules are already defined for buttons and tabs and so on and we defined additional rules for them. When changing the global skin, you would need to merge the changes into the existing rules.
For the best skinnability, it is best to declare appearance related style rules in the global directory rather than in individual style files. This includes colors, fonts and general widget appearances. If you change the color of something in a local skin file (such as findfile.css), the dialog may look odd if the user changes their global skin. Don't expect the user to be using the default one.
XUL and XML provide entities which are a convenient way of allowing localization.
Many applications are built such that translating the interface into a different language is as simple as possible. Usually, a table of strings is created for each language. Instead of hard-coding text directly into an application, each piece of text is only a reference into the string table. XML provides entities which can be used for a similar purpose.
You should already be familiar with entities if you have written HTML. The codes < and > are examples of entities which can be used to place less than and greater than signs into the text. XML has a syntax which allows you to declare custom entities. You can use these so that the entity is replaced with its value, which can be a string of text. Entities may be used whenever text occurs, including the values of attributes. The example below demonstrates the use of an entity in a button.
<button label="&findLabel"/>
The text that will appear on the label will be the value that the entity &findLabel has. A file is created which contains the entity declarations for each supported language. In English, the &findLabel entity will probably be declared to have the text 'Find'.
Entities are declared in DTD (Document Type Declaration) files. These types of files are normally used to declare the syntax and sematics of a particular XML file, but they also allow you to declare entities. In the Mozilla chrome system, you will find DTD files located in the locales subdirectory. You would normally have one DTD file (with an extension dtd) per XUL file.
If you look in the chrome directory, you should see an archive for your language. (en-US.jar is the default for English.) You might have locale files in multiple languages, for example, US English (en-US) and French (fr). Inside these archives, you will find the files that hold the localized text for each window. The structure of the archives is very similar to the directory structure used for skins.
Inside the archives, you would place your DTD files in which you declare entities. Typically, you will have one DTD file for each XUL file, usually with the same filename except with a .dtd extension. So, for the find files dialog, we will need a file called findfile.dtd.
For non-installed chrome files, you can just put the DTD file in the same directory as the XUL file.
Once you have created a DTD file for your XUL, you will need to add a line to the XUL file which indicates that you want to use the DTD file. Otherwise, errors will occur as it won't be able to find the entities. To do this, add a line of the following form somewhere near the top of the XUL file:
<!DOCTYPE window SYSTEM "chrome://findfile/locale/findfile.dtd">
This line specifies that the URL indicated is to be used as a DTD for the file. In this case, we have declared that we want to use the findfile.dtd DTD file. This line is typically placed just before the window element.
The entities are declared using a simple syntax as shown below:
<!ENTITY findLabel "Find">
This example creates an entity with the name findLabel and the value Find. This means that wherever the text &findLabel appears in the XUL file, it will be replaced with the text Find. In the DTD file for a different language, the text for that language will be used instead. Note that entity declarations do not have a trailing slash at the end of them.
For example, the following text:
<description value="&findLabel"/>
is translated as:
<description value="Find"/>
You would declare an entity for each label or string of text that you use in your interface. You should not have any directly displayed text in the XUL file at all.
In addition to using entities for text labels, you should use them for any value that could be different in a different language. Access keys and keyboard shortcuts for example.
<menuitem label="&undo.label" accesskey="&undo.key"/> <!ENTITY undo.label "Undo"> <!ENTITY undo.key "u">
The example above uses two entities, one for the label on the Undo menu item and the second for the access key.
Let's take a look at how we would put all of this together by modifying the find files dialog so that it uses a DTD file for all of its text strings. The entire XUL file is shown below with the changes shown in red.
<?xml version="1.0"?> <?xml-stylesheet href="findfile.css" type="text/css"?> <!DOCTYPE window SYSTEM "chrome://findfile/locale/findfile.dtd"> <window id="findfile-window" title="&findWindow.title;" persist="screenX screenY width height" orient="horizontal" onload="initSearchList()" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script src="findfile.js"/> <popupset> <popup id="editpopup"> <menuitem label="Cut" accesskey="&cutCmd.accesskey;"/> <menuitem label="Copy" accesskey="©Cmd.accesskey;"/> <menuitem label="Paste" accesskey="&pasteCmd.accesskey;" disabled="true"/> </popup> </popupset> <keyset> <key id="cut_cmd" modifiers="accel" key="&cutCmd.commandkey;"/> <key id="copy_cmd" modifiers="accel" key="©Cmd.commandkey;"/> <key id="paste_cmd" modifiers="accel" key="&pasteCmd.commandkey;"/> <key id="close_cmd" keycode="VK_ESCAPE" oncommand="window.close();"/> </keyset> <vbox flex="1"> <toolbox> <menubar id="findfiles-menubar"> <menu id="file-menu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;"> <menupopup id="file-popup"> <menuitem label="&openCmd.label;" accesskey="&openCmd.accesskey;"/> <menuitem label="&saveCmd.label;" accesskey="&saveCmd.accesskey;"/> <menuseparator/> <menuitem label="&closeCmd.label;" accesskey="&closeCmd.accesskey;" key="close_cmd" oncommand="window.close();"/> </menupopup> </menu> <menu id="edit-menu" label="&editMenu.label;" accesskey="&editMenu.accesskey;"> <menupopup id="edit-popup"> <menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" key="cut_cmd"/> <menuitem label="©Cmd.label;" accesskey="©Cmd.accesskey;" key="copy_cmd"/> <menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" key="paste_cmd" disabled="true"/> </menupopup> </menu> </menubar> <toolbar id="findfiles-toolbar"> <toolbarbutton id="opensearch" label="&openCmdToolbar.label;"/> <toolbarbutton id="savesearch" label="&saveCmdToolbar.label;"/> </toolbar> </toolbox> <tabbox> <tabs> <tab label="&searchTab;" selected="true"/> <tab label="&optionsTab;"/> </tabs> <tabpanels> <tabpanel id="searchpanel" orient="vertical" context="editpopup"> <description> &findDescription; </description> <spacer class="titlespace"/> <groupbox orient="horizontal"> <caption label="&findCriteria;"/> <menulist id="searchtype"> <menupopup> <menuitem label="&type.name;"/> <menuitem label="&type.size;"/> <menuitem label="&type.date;"/> </menupopup> </menulist> <spacer class="springspace"/> <menulist id="searchmode"> <menupopup> <menuitem label="&mode.is;"/> <menuitem label="&mode.isnot;"/> </menupopup> </menulist> <spacer class="springspace"/> <menulist id="find-text" flex="1" editable="true" datasources="file:///mozilla/recents.rdf" ref="urn:findfile:recent"> <template> <menupopup> <menuitem label="rdf:http://www.example.com/recent#Label" uri="rdf:*"/> </menupopup> </template> </menulist> </groupbox> </tabpanel> <tabpanel id="optionspanel" orient="vertical"> <checkbox id="casecheck" label="&casesensitive;"/> <checkbox id="wordscheck" label="&matchfilename;"/> </tabpanel> </tabpanels> </tabbox> <tree id="results" style="display: none;" flex="1"> <treecols> <treecol id="name" label="&results.filename;" flex="1"/> <treecol id="location" label="&results.location;" flex="2"/> <treecol id="size" label="&results.size;" flex="1"/> </treecols> <treechildren> <treeitem> <treerow> <treecell label="mozilla"/> <treecell label="/usr/local"/> <treecell label="&bytes.before;2520&bytes.after;"/> </treerow> </treeitem> </treechildren> </tree> <splitter id="splitbar" resizeafter="grow" style="display: none;"/> <spacer class="titlespace"/> <hbox> <progressmeter id="progmeter" value="50%" style="display: none;"/> <spacer flex="1"/> <button id="find-button" label="&button.find;" default="true" oncommand="doFind()"/> <button id="cancel-button" label="&button.cancel;" oncommand="window.close();"/> </hbox> </vbox> </window>
Each text string has been replaced by an entity reference. A DTD file has been included near the beginning of the XUL file. Each entity that was added should be declared in the DTD file. The window will not be displayed if an entity is found in the XUL file that hasn't been declared.
Note that the name of the entity is not important. In the example above, words in entities have been separated with periods. You don't have to do this. The entity names here follow similar conventions as the rest of the Mozilla code.
You might notice that the text '2520 bytes' has been replaced by two entities. This is because the phrase structure may be different in another locale. For example, the number might need to appear before the equivalent of 'bytes' instead of after. Of course, this might need to be more complicated in order to display KB or MB as needed.
The access keys and keyboard shortcuts have also been translated into entities because they will likely be different in a different locale.
Next, the DTD file (findfile.dtd):
<!ENTITY findWindow.title "Find Files"> <!ENTITY fileMenu.label "File"> <!ENTITY editMenu.label "Edit"> <!ENTITY fileMenu.accesskey "f"> <!ENTITY editMenu.accesskey "e"> <!ENTITY openCmd.label "Open Search..."> <!ENTITY saveCmd.label "Save Search..."> <!ENTITY closeCmd.label "Close"> <!ENTITY openCmd.accesskey "o"> <!ENTITY saveCmd.accesskey "s"> <!ENTITY closeCmd.accesskey "c"> <!ENTITY cutCmd.label "Cut"> <!ENTITY copyCmd.label "Copy"> <!ENTITY pasteCmd.label "Paste"> <!ENTITY cutCmd.accesskey "t"> <!ENTITY copyCmd.accesskey "c"> <!ENTITY pasteCmd.accesskey "p"> <!ENTITY cutCmd.commandkey "X"> <!ENTITY copyCmd.commandkey "C"> <!ENTITY pasteCmd.commandkey "V"> <!ENTITY openCmdToolbar.label "Open"> <!ENTITY saveCmdToolbar.label "Save"> <!ENTITY searchTab "Search"> <!ENTITY optionsTab "Options"> <!ENTITY findDescription "Enter your search criteria below and select the Find button to begin the search."> <!ENTITY findCriteria "Search Criteria"> <!ENTITY type.name "Name"> <!ENTITY type.size "Size"> <!ENTITY type.date "Date Modified"> <!ENTITY mode.is "Is"> <!ENTITY mode.isnot "Is Not"> <!ENTITY casesensitive "Case Sensitive Search"> <!ENTITY matchfilename "Match Entire Filename"> <!ENTITY results.filename "Filename"> <!ENTITY results.location "Location"> <!ENTITY results.size "Size"> <!ENTITY bytes.before ""> <!ENTITY bytes.after "bytes"> <!ENTITY button.find "Find"> <!ENTITY button.cancel "Cancel">
Now, to change a language all you need to do is create another DTD file. By using the chrome system to add the DTD file to a different locale, the same XUL file can be used in any language.
In a script, entities cannot be used. Property files are used instead.
DTD files are suitable when you have text in a XUL file. However, a script does not get parsed for entities. In addition, you may wish to display a message which is generated from a script, if, for example, you do not know the exact text to be displayed. For this purpose, property files can be used.
A property file contains a set of strings . You will find property files alongside the DTD files with a .properties extension. Properties in the file are declared with the syntax name=value. An example is shown below:
notFoundAlert=No files were found matching the criteria. deleteAlert=Click OK to have all your files deleted.
Here, the property file contains two properties. These would be read by a script and displayed to the user. You could write the code to read properties yourself, however XUL provides the stringbundle element which does this for you. The element has a number of functions which can be used to get strings from the property file and get other locale information. This element reads in the contents of a property file and builds a list of properties for you. You can then look up a particular property by name.
<stringbundle id="strings" src="strings.properties"/>
Including this element will read the properties from the file 'strings.properties' in the same directory as the XUL file. Use a chrome URL to read a file from the locale.
This stringbundle element has a number of properties. The first is getString which can be used in a script to read a string from the bundle.
var strbundle=document.getElementById("strings");
var nofilesfound=strbundle.getString("notFoundAlert");
alert(nofilesfound);This example first gets a reference to the bundle using its id. Then, it looks up the string 'notFoundAlert' in the property file. The function getString returns the value of the string or null if the string does not exist. Finally, the string is displayed in an alert box.
XUL has a sister language, XBL (eXtensible Bindings Language). This language is used for declaring the behavior of XUL widgets.
You can use XUL to define the layout of a user interface for an application. You can customize the look of elements by applying styles to them. You can also create new skins by changing the styles. The basic appearance of all elements, such as scroll bars and check boxes may be modified by adjusting the style or by setting attributes on the element. However, XUL provides no means in which you can change how an element works. For example, you might want to change how the pieces of a scroll bar function. For this, you need XBL.
An XBL file contains a set of bindings. Each binding describes the behavior of a XUL widget. For example, a binding might be attached to a scroll bar. The behavior describes the properties and methods of the scroll bar in addition to describing the XUL elements that make up a scroll bar.
Like XUL, XBL is an XML language, so it has similar syntax rules. The following example shows the basic skeleton of an XBL file:
<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl">
<binding id="binding1">
<!-- content, property, method and event descriptions go here -->
</binding>
<binding id="binding2">
<!-- content, property, method and event descriptions go here -->
</binding>
</bindings>The bindings element is the root element of an XBL file and contains one or more binding elements. Each binding element declares a single binding. The id attribute can be used to identify the binding, as in the example above. The template has two bindings, one called binding1 and the other called binding2. One might be attached to a scroll bar and the other to a menu. A binding can be attached to any XUL element. If you use CSS classes, you can use as many different bindings as you need. Note the namespace on the bindings element in the template above. This declares that we are using XBL syntax.
You assign a binding to an element by setting the CSS property -moz-binding to the URL of the bindings file. For example:
scrollbar {
-moz-binding: url('chrome://findfile/content/findfile.xml#binding1');
}The URL points to the binding with the id 'binding1' in the file 'chrome://findfile/content/findfile.xml'. The '#binding1' syntax is used to point to a specific binding, much like how you would point to an anchor in an HTML file. You will usually put all of your bindings in a single file. The result in this example, is that all scrollbar elements will have their behavior described by the binding 'binding1'. If you don't use an anchor in the -moz-binding URL, the first binding in the XBL file is used.
A binding has four types of things that it declares:
The box is generic enough that you can use it to create custom widgets (although you can use any element, even one you make up yourself). By assigning a class to a box tag, you can associate a binding to just boxes that belong to that class. The following example demonstrates this.
XUL (example.xul):
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://example/skin/example.css" type="text/css"?>
<window
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<box class="okcancelbuttons"/>
</window>
CSS (example.css):
box.okcancelbuttons {
-moz-binding: url('chrome://example/skin/example.xbl#okcancel');
}
XBL (example.xbl):
<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="okcancel">
<content>
<xul:button label="OK"/>
<xul:button label="Cancel"/>
</content>
</binding>
</bindings>This example creates a window with a single box. The box has been declared to have a class of okcancelbuttons. The style sheet associated with the file says that boxes with the class okcancelbuttons have a specialized binding, defined in the XBL file. You may use other elements besides the box, even your own custom tags.
We'll look more at the details of the XBL part in the next section. However, to summarize, it causes two buttons to be added automatically inside the box, one an OK button and the other a Cancel button.
In this section we'll look at creating content with XBL.
XBL can be used to automatically add a set of elements inside another element. The XUL file only needs to specify the outer element while the inner elements are described in the XBL. This is useful for creating a single widget that is made up of a set of other widgets, but can be referred to as only a single widget. Mechanisms are provided for adding attributes to the inner elements that were specified on the outer element.
The example below shows how a scrollbar might be declared (It has been simplified a bit from the real thing):
<bindings xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="scrollbarBinding">
<content>
<xul:scrollbarbutton type="decrement"/>
<xul:slider flex="1">
<thumb/>
<xul:/slider>
<xul:scrollbarbutton type="increment"/>
</content>
</binding>
</bindings>This file contains a single binding, declared with the binding element. The id attribute should be set to the identifier of the binding. This way it can be referred to through the CSS -moz-binding property.
The content tag is used to declare anonymous content that will be added to the scroll bar. All of the elements inside the content tag will be added inside the element that the binding is bound to. Presumably this binding would be bound to a scroll bar, altough it doesn't have to be. Any element that has its CSS -moz-binding property set to the URI of the binding will use it.
The result of using the above binding is that the line of XUL below will be expanded as follows, assuming that the scrollbar is bound to the XBL above:
<scrollbar>
expands to:
<scrollbar>
<xul:scrollbarbutton type="decrement"/>
<xul:slider flex="1"/>
</xul:thumb>
</xul:slider>
<xul:scrollbarbutton type="increment"/>
</scrollbar>The elements within the content tag are added to the scroll bar anonymously. Although anonymous content is displayed on screen, you cannot get to it through a script in the normal way. To the XUL, it's as if there was only one single element, even though it is really made up of a number of elements.
If you look at a scroll bar in a Mozilla window, you will see that it is made up of an arrow button, a slider, a thumb inside it and a second arrow button at the end, which are the elements that appear in the XBL above. These elements would in turn be bound to other bindings that use the base XUL elements. Notice that the content elements need the XUL namespace (they appear preceded with xul:), because they are XUL elements and aren't valid in XBL. This namespace was declared on the bindings tag. If you don't use the namespace on XUL elements, Mozilla will assume that the elements are XBL, not understand them, and your elements won't work correctly.
Another example, this time for a field for entering a filename:
<binding id="fileentry"> <textbox/> <button label="Browse..."/> </binding>
Attaching this binding to an element will cause it to contain a field for entering text, followed by a Browse button. This inner content is created anonymously and cannot be seen using the DOM.
The anonymous content is created automatically whenever a binding is attached to an element. If you place child elements inside the XUL, they will override the elements provided by the binding. For example, take this XUL fragment, assuming it is bound to the scrollbar XBL earlier:
<scrollbar/> <scrollbar> <button label="Overridden"/> </scrollbar>
The first scroll bar, because it has no content of its own, will have its content generated from a binding definition declared in an XBL file. The second scroll bar has its own content so it will use that instead of the XBL content, resulting in something that isn't much of a scroll bar at all. Note that the built-in elements such as scroll bars, get their XBL from the files in the bindings directory in the toolkit package.
This only applies to the elements defined within the content tag. Properties, methods and other aspects of XBL are still available whether the content is from XBL or whether the XUL provides its own content.
There may be times when you want both the XBL content and the content provided by the XUL file to be displayed. You can do this by using the children element. The children added in the XUL are added in place of the children element. This is handy when creating custom menu widgets. For example, a simplified version of an editable menulist element, might be created as follows:
XUL:
<menu class="dropbox">
<menupopup>
<menuitem label="1000"/>
<menuitem label="2000"/>
</menupopup>
</menu>
CSS:
box.dropbox {
-moz-binding: url('chrome://example/skin/example.xbl#dropbox');
}
XBL:
<binding id="dropbox">
<content>
<children/>
<xul:textbox flex="1"/>
<xul:button src="chrome://global/skin/images/dropbox.jpg"/>
</content>
</binding>This example creates an input field with a button beside it. The menupopup will be added to the content in the location specified by the children element. Note that to DOM functions, the content will appear as it was in the XUL file, so the menupopup will be a child of the menu. The XBL content is hidden away so the XUL developer doesn't need to even know it is there.
The resulting content would be:
<menu class="dropbox">
<menupopup>
<menuitem label="1000"/>
<menuitem label="2000"/>
</menupopup>
<textbox flex="1"/>
<button src="chrome://global/skin/images/dropbox.jpg"/>
</menu>In some cases, you may wish to only include specific types of content and not others. Or, you may wish to place different types of content in different places. The includes attribute can be used to allow only certain elements to appear in the content. Its value should be set to a single tag name, or to a list of tags separated by vertical bars ( The | symbol ).
<children includes="button">
This line will add all buttons that are children of the bound element in place of the children tag. Other elements will not match this tag. You can place multiple children elements in a binding to place different types of content in different places. If an element in the XUL does not match any of the children elements, that element (and any others that don't match) will be used instead of the bound content.
Here is another example. Let's say that we wanted to create a widget that displayed an image with a zoom in and zoom out button on each side of it. This would be created with a box to hold the image and two buttons. The image element has to placed outside the XBL as it will differ with each use.
XUL:
<box class="zoombox">
<image src="images/happy.jpg"/>
<image src="images/angry.jpg"/>
</box>
XBL:
<binding id="zoombox">
<content>
<xul:box flex="1">
<xul:button label="Zoom In"/>
<xul:box flex="1" style="border: 1px solid black">
<children includes="image"/>
</xul:box>
<xul:button label="Zoom Out"/>
</xul:box>
</content>
</binding>The explicit children in the XUL file will be placed at the location of the children tag. There are two images, so both will be added next to each other. This results in a display that is equivalent to the following:
<binding id="zoombox">
<content>
<xul:box flex="1">
<xul:button label="Zoom In"/>
<xul:box flex="1" style="border: 1px solid black">
<image src="images/happy.jpg"/>
<image src="images/angry.jpg"/>
</xul:box>
<xul:button label="Zoom Out"/>
</xul:box>
</content>
</binding>From the DOM's perspective, the child elements are still in their original location. That is, the outer XUL box has two children, which are the two images. The inner box with the border has one child, the children tag. This is an important distinction when using the DOM with XBL. This also applies to CSS selector rules.
You can also use multiple children elements and have certain elements be placed in one location and other elements placed in another. By adding a includes attribute and setting it to a bar-separated list of tags, you can make only elements in that list be placed at that location. For example, the following XBL will cause text labels and buttons to appear in a different location than other elements:
Example 10.2.1: Source<binding id="navbox">
<content>
<xul:vbox>
<xul:label value="Labels and Buttons"/>
<children includes="label|button"/>
</xul:vbox>
<xul:vbox>
<xul:label value="Other Elements"/>
<children/>
</xul:vbox>
</content>
</binding>The first children element only grabs the label and button elements, as indicated by its includes attribute. The second children element, because it has no includes attribute, grabs all of the remaining elements.
In this section we'll see how attributes can be inherited.
XBL allows us to build composite widgets while hiding their actual implementation. However, with the features mentioned so far, the anonymous content is always created in the same way. It would be useful to add attributes to the bound elements that modify the inner elements. For example:
XUL:
<searchbox/>
XBL:
<binding id="searchBinding">
<content>
<xul:textbox/>
<xul:button label="Search"/>
</content>
</binding>In the example, the label attribute has been placed directly on the button element. The problem with this is that the label would be the same every time the binding was used. XBL provides an inherits attribute which can be used to inherit attributes from the bound element. It should be placed on the element that should inherit an attribute from the outer element, in this case the button. Its value should be set to a comma-separated list of attribute names that are to be inherited.
<xul:textbox xbl:inherits="flex"/> <xul:button xbl:inherits="label"/>
When the content is generated, the textbox grabs the flex attribute from the searchbox and the button grabs the label attribute from the searchbox. This allows both the flexibility of the textbox and the label of the button to be different for each use of the binding. In addition, changing the value of the attributes on the searchbox will update the textbox and button also. You can add the inherits attribute to as many elements as you wish, to inherit any number of attributes.
Note how the inherits attribute has been placed in the XBL namespace, by prefixing it with 'xbl:'. The namespace should be declared somewhere earlier, usually on the bindings element. The next example demonstrates this.
<bindings xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<xbl:binding id="buttonBinding">
<xbl:content>
<xul:button label="OK" xbl:inherits="label"/>
</xbl:content>
</xbl:binding>In this example, the button inherits the label attribute, but this attribute is also given a value directly in the XBL. This technique is used to set the default value if the attribute is not present. This button will inherit its label attribute from the outer element. However, if no label is present, it will be given a default value of OK.
There may be times where two generated elements need to inherit from an attribute that has the same name. For example, to create a labeled textbox (a textbox with a text description beside it) out of a label and a textbox element, the label will need to inherit its text from the value attribute and the textbox will also need to inherit its default value from the value attribute as well. To solve this, we will need to use a different attribute and map it to the same one. The following demonstrates this:
XUL:
<box class="labeledtextbox" title="Enter some text:" label="OK"/>
CSS:
box.labeledtextbox {
-moz-binding: url('chrome://example/skin/example.xbl#labeledtextbox');
}
XBL:
<binding id="labeledtextbox">
<content>
<xul:label xbl:inherits="value=title"/>
<xul:textbox xbl:inherits="value"/>
</content>
</binding>The textbox inherits the value attribute directly. To set the value attribute on the label, we need to use a different attribute name and map it to the value. The inherits attribute on the label grabs the title attribute from the labeledtextbox and maps it to the value attribute of the label element. The syntax <inner attribute>=<outer attribute> is used to map one attribute to another. Here is another example:
XUL:
<box class="okcancel" oktitle="OK" canceltitle="Cancel" image="happy.png"/>
CSS:
box.okcancel {
-moz-binding: url('chrome://example/skin/example.xbl#okcancel');
}
XBL:
<binding id="okcancel">
<content>
<xul:button xbl:inherits="label=oktitle,image"/>
<xul:button xbl:inherits="label=canceltitle"/>
</content>
</binding>The value of the oktitle attribute is mapped to the label attribute of the first button. The canceltitle attribute is mapped to the label attribute of the second button. The first button also inherits the image attribute. The result is as follows:
<box class="okcancel" oktitle="OK" canceltitle="Cancel" image="happy.png"> <button label="OK" image="happy.png"/> <button label="Cancel"/> </box>
Note that the attributes are duplicated on the inner (anonymous) content. Changing the attributes on the box with the okcancel class will automatically change the values on the buttons. You may also have noticed that we just made up our own attribute names. This is valid in XUL.
Next, we'll find out how to add custom properties to XBL-defined elements.
JavaScript and the DOM provide access to get and set the properties of elements. With XBL, you can define your own properties for the elements you create. You can also add methods that can be called. That way, all you need is to get a reference to the element (using GetElementById or a similar function) and then get or set the additional properties and call the methods on it.
There are three types of items you can add. Fields are used to hold a simple value. Properties can also be used to hold a value but may have code execute when an attempt is made to retrieve or modify the value. Methods are functions which may be executed.
All three are defined within an implementation element, which should be a child of the binding element. Within the implementation, you define individual field, property, and method elements, one for each one that you want. The general syntax is as follows:
<binding id="element-name">
<content>
-- content goes here --
</content>
<implementation>
<field name="field-name-1"/>
<field name="field-name-2"/>
<field name="field-name-3"/>
<property name="property-name-1"/>
<property name="property-name-2"/>
<property name="property-name-3"/>
.
.
.
<method name="method-name-1/>
-- method content goes here --
</method>
.
.
.
</implementation>
</binding>Each field is defined using the field element. Often, fields would correspond to an attribute placed on the element such as label or disabled, but they do not have to.
The name attribute on the field element is used to indicate the name of the field. You can use the name from a script to get and set the value. The example below creates a button which generates and stores a random number. You can retrieve this same number multiple times by getting the number property from the button. Most of the work here is done in the onclick handlers. Later, we'll find out how to move this to XBL.
XUL:
<box id="random-box" class="randomizer"/>
<button label="Generate"
onclick="document.getElementById('random-box').number=Math.random();"/>
<button label="Show"
onclick="alert(document.getElementById('random-box').number)"/>
XBL:
<binding id="randomizer">
<implementation>
<field name="number"/>
</implementation>
</binding>A number field has been defined in the binding, which stores the random number. The two extra buttons set and get the value of this field. The syntax is very similar to getting and setting the properties of HTML elements. In this example, no content has been placed inside either the XUL box or its definition in XBL, which is perfectly valid.
This example isn't quite correct because the field is not assigned a default value. To do this, add the default value as the content of the field tag. For example:
<field name="number"> 25 </field>
This will assign the value 25 as the default value of the number field. Actually, you can instead place a script inside the field tag that evaluates to the default value. That might be necessary if the value needs to be computed. For example, the following field is given a default value equal to the current time:
<field name="currentTime"> new Date().getTime(); </field>
Sometimes you will want to validate the data that is assigned to a property. Or, you may want the value to be calculated dynamically as it's asked for. For example, if you want a property that holds the current time, you would want to have its value generated as needed. In these cases, you need to use a property tag instead of a field tag. Its syntax is similar but has additional features.
You can use the onget and onset attributes to have code execute when the property is retrieved or modified. Add each to the property element and set its value to a script which either gets or sets the value of the property.
For example, you could assign a script to the value of onget to calculate the current time. Whenever a script attempts to access the value of the property, the onget script will be called to retrieve the value. The script should return the value that should be treated as the value of that property.
The onset handler is similar but is called whenever a script attempts to assign a new value to the property. This script should store the value somewhere, or validate the value. For example, some properties might only be able to store numbers. Attempting to assign alphabetic text to such a property should fail.
<property name="size"
onget="return 77;"
onset="alert('Changed to:'+val); return val;"/>This property will always return 77 when retrieved. When set, an alert will be displayed which displays the value to assign to the property. The special variable val holds the value that the property should be assigned to. Use this to validate it or store it. The onset property should also return the new value.
The following decribes what happens in a typical case:
There are two elements, one called 'banana' and the other 'orange'. They each have a custom property called 'size'. When the following line of script is executed:
banana.size = orange.size;
Note that unlike a field, a property does not hold a value. Attempting to set a property that does not have an onset handler will generate an error. You will often use a separate field to hold the actual value of the property. It is also common to have the properties match an attribute on the XBL-defined element. The following example maps a property to an attribute on an element.
<property name="size"
onget="return this.getAttribute('size');"
onset="return this.setAttribute('size',val);"
/>Whenever a script attempts to get the value of the property, it is grabbed instead from the attribute on the element with the same name. Whenever a script attempts to set the value of a property, it is set as an attribute on the element. This is convenient because then you can modify the property or the attribute and both will have the same value.
You can use an alternate syntax for the onget and onset attributes that is useful if the scripts are longer. You can replace the onget attribute with a child element called getter. Similarly, you can replace the onset attribute with a setter element. The example below shows this:
<property name="number">
<getter>
return this.getAttribute('number');
</getter>
<setter>
var v=parseInt(val);
if (!isNaN(v)) return this.setAttribute('number',''+v);
else return this.getAttribute('number');"
</setter>
</property>Th property in this example will only be able to hold integer values. If other characters are entered, they are stripped off. If there are no digits, the value is not changed. This is done in the code inside the setter element. The real value of the property is stored in the number attribute.
You can use either syntax for creating get and set handlers.
You can make a field or property read-only by adding a readonly attribute to the field tag or property tag and setting it to true. Attempting to set the value of a read-only property will fail.
Next, we'll find out how to add custom methods to XBL-defined elements.
In addition to adding script properties to the XBL-defined element, you can also add methods. These methods can be called from a script. Methods are the functions of objects, such as 'window.open()'. You can define custom methods for your elements using the method element. The general syntax of methods is as follows:
<implementation>
<method name="method-name">
<parameter name="parameter-name1"/>
<parameter name="parameter-name2"/>
.
.
.
<body>
-- method script goes here --
</body>
</method>
</implementation>A method declaration goes inside the implementation element, like the fields and properties do. The method element contains two type of child elements, parameter elements which describe the parameters to the method and body which contains the script for the method.
The value of the name attribute becomes the name of the method. Similarly, the name attributes on the parameter elements become the names of each parameter. Each parameter element is used to declare one parameter for the method. For example, if the method had three parameters, there would be three parameter elements. You do not need to have any though, in which case the method would take no parameters.
The body element contains the script that is executed when the method is called. The names of the parameters are defined as variables in the script as if they had been passed as parameters. For example, the following JavaScript function would be written as an XBL method like so:
function getMaximum(num1,num2)
{
if (num1<=num2) return num2;
else return num1;
}
XBL:
<method name="getMaximum">
<parameter name="num1"/>
<parameter name="num2"/>
<body>
if (num1<=num2) return num2;
else return num1;
</body>
<method>This function, getMaximum, returns the largest of the values, each passed as an parameter to the method. Note that the less-than symbol had to be escaped because otherwise it would look like the start of a tag. You can call the method by using code such as 'element.getMaximum(5,10)' where element is a reference to an element defined by the XBL containing the getMaximum method. (The bound element.)
The parameter tag allows you to define parameters for a method. Because Mozilla uses JavaScript as its scripting language, and JavaScript is a non-typed language, you do not need to specify the types of the parameters. However, in the future, other languages may be used with XBL.
There may be times when you want to modify some aspect of the elements defined in the content element, either from a method body or elsewhere. These elements are created anonymously and are not accessible from the regular DOM functions. They are hidden so that developers do not need to know how the element is implemented to use it. However, there is a special way of getting this anonymous content.
Elements with an XBL behavior attached to them have a special property which holds an array of the anonymous child elements inside it. Each element of the array stores each direct child element of the XBL-defined element. This special property cannot be accessed directly. Instead, you must call the document's getAnonymousNodes method:
var value=document.getAnonymousNodes(element);
Here, 'element' should be set a to reference to the element that you want to get the anonymous content of. The function returns an array of elements, which is the anonymous content. To get elements below that, you can use the regular DOM functions because they aren't hidden. Note that it is possible for an XBL-bound element to be placed inside another one, in which case you will have to use the getAnonymousNodes function again.
The following example creates a row of buttons:
<binding id="buttonrow">
<content>
<button label="Yes"/>
<button label="No"/>
<button label="Sort Of"/>
</content>
</binding>To refer to each button, you can use the getAnonymousNodes function, passing it a reference to the element the binding is bound to as the parameter. In the returned array, the first button is stored in the first array element ('getAnonymousNodes(element)[0]'), the second button is stored in the second array element and the third button is stored in the third array element. For code inside a binding method, you can pass 'this' as the parameter to getAnonymousNodes.
The next example can be used to create text with a label. The method 'showTitle' can be used to show or hide the label. It works by getting a reference to the title element using the anonymous array and changing the visibility of it.
XUL:
<box id="num" class="labeledbutton" title="Number of Things:" value="52"/>
<button label="Show" onclick="document.getElementById('num').showTitle(true)"/>
<button label="Hide" onclick="document.getElementById('num').showTitle(false)"/>
XBL:
<binding id="labeledbutton">
<content>
<xul:label xbl:inherits="value=title"/>
<xul:label xbl:inherits="value"/>
</content>
<implementation>
<method name="showTitle">
<parameter name="state"/>
<body>
if (state) document.getAnonymousNodes(this)[0].setAttribute("style","visibility: visible");
else document.getAnonymousNodes(this)[0].setAttribute("style","visibility: collapse");
</body>
</method>
</implementation>
</binding>Two buttons added to the XUL have onclick handlers which are used to change the visibility of the label. Each calls the 'showTitle' method. This method checks to see whether the element is being hidden or shown from the 'state' parameter that is passed in. In either case, it grabs the first element of the anonymous array. This refers to the first child in the content element, which here is the first label widget. The visibility is changed by modifying the style on the element.
To go the other way, and get the bound element from inside the anonymous content, use the DOM 'parentNode' property. This gets the parent element of an element. For example, we could move the Show and Hide buttons into the XBL file and do the following:
Example 10.5.1: Source<binding id="labeledbutton">
<content>
<xul:label xbl:inherits="value=title"/>
<xul:label xbl:inherits="value"/>
<xul:button label="Show" onclick="parentNode.showTitle(true);"/>
<xul:button label="Hide" onclick="parentNode.showTitle(false);"/>
</content>
<implementation>
<method name="showTitle">
<parameter name="state"/>
<body>
if (state) document.getAnonymousNodes(this)[0].setAttribute("style","visibility: visible");
else document.getAnonymousNodes(this)[0].setAttribute("style","visibility: collapse");
</body>
</method>
</implementation>
</binding>The onclick handlers here first get a reference to their parent element. This is not the content element but the XUL element that the XBL is bound to. (In this example, it is the box with the labeledbutton class). Then, the 'showTitle' method is called, which functions as it did before.
Custom properties and methods are added only to the outer XUL element the XBL is bound to. None of the elements declared inside the content tag have these properties or methods. This is why we have to get the parent first.
The children of an element placed in the XUL file can be retrieved in the normal way and don't move even if you use the children tag. For example:
XUL:
<box id="outer" class="container">
<button label="One"/>
<button label="Two"/>
<button label="Three"/>
<button label="Four"/>
</box>
XBL:
<binding id="labeledbutton">
<content>
<description value="A stack:"/>
<stack>
<children/>
</stack>
</content>
</binding>If you use the DOM functions such as 'childNodes' to get the children of an element, you'll find that the XUL box, the one with the id of outer, has 4 children. These correspond to its four buttons, even through those buttons are drawn inside the stack. The stack has only one child, the children element itself. The length of the anonymous array of the outer box is two, the first element the description element and the second the stack element.
XBL supports two special methods created with separate tags, constructor and destructor. A constructor is called whenever the binding is attached to an element. It is used to initialize the content such as loading preferences or setting the default values of fields. The destructor is called when a binding is removed from an element. This might be used to save information.
There are two points when a binding is attached to an element. The first occurs when a window is displayed. All elements that have XBL-bound content will have their constructors invoked. The order that they are called in should not be relied upon, as they are loaded from various files. The window's onload handler is not called until after all the bindings have been attached and their constructors finished. The second point a binding is attached is if you change the -moz-binding style property of an element. The existing binding will be removed, after its destructor is called. Then, the new binding will be added in its place and its constructor invoked.
The script for a constructor or destructor should be placed directly inside the appropriate tag. There should only be at most one of each per binding and they take no arguments. Here are some examples:
<constructor>
if (this.childNodes[0].getAttribute("open") == "true"){
this.loadChildren();
}
</constructor>
<destructor action="saveMyself(this);"/>Next, we'll find out how to add event handlers to XBL-defined elements.
As you might expect, mouse clicks, key presses and other events are passed to each of the elements inside the content. However, you may wish to trap the events and handle them in a special way. You can add event handlers to the elements inside the content if needed. The last example in the previous section demonstrated this. In that example, onclick handlers were added to some buttons.
However, you may want to add an event handler to the entire contents, that is, all the elements defined in the content tag. This could be useful when trapping the focus and blur events. To define an event handler, use the handler element. Each will describe the action taken for a single event handler. You can use more than one handler if necessary. If an event does not match any of the handler events, it is simply passed to the inner content as usual.
The general handler syntax is as follows:
<binding id="binding-name">
<handlers>
<handler event="event-name" action="script"/>
</handlers>
</binding>Place all of your handlers within the handlers element. Each handler element defines the action taken for a particular event specified by its event attribute. Valid event types are those supported by XUL and JavaScript, such as click and focus. Use the event name without the 'on' in front of it.
A common reason to set handlers is to modify the custom properties when an event occurs. For example, a custom checkbox might have a checked property which needs to be changed when the user clicks the checkbox:
<handlers> <handler event="mouseup" action="this.checked=!this.checked"/> </handlers>
When the user clicks and releases the mouse button over the check box, the mouseup event is sent to it, and the handler defined here is called, causing the state of the checked property to be reversed. Similarly, you may wish to change a property when the element is focused. You will need to add handlers to adjust the properties whenever input from the mouse or keyboard would require it.
For mouse events, you can use the button attribute to have the handler only trap events that occur from a certain button. Without this attribute, the handler traps all events regardless of the button that was pressed. The button attribute should be set to either 0 for the left mouse button, 1 for the middle mouse button or 2 for the right mouse button.
<handlers>
<handler event="click" button="0" action="alert('Left button pressed');"/>
<handler event="mouseup" button="1" action="alert('Middle button pressed')"/>
<handler event="click" button="2" action="alert('Right button pressed');"/>
</handlers>For key events, you can use a number of attributes similar to those for the key element to match a specific key and match only when certain modifer keys are pressed. The previous example could be extended so that the checked property of the check box is changed when the space bar is pressed.
<handlers> <handler event="keypress" key=" " action="this.checked=!checked"/> </handlers>
You can also use the keycode attribute to check for non-printable keys. The section on keyboard shortcuts provides more information. The modifier keys can be checked by adding a modifiers attribute. This should be set to one of the values set below:
If set, the handler is only called when the modifier is pressed. You can require multiple modifier keys by separating them with spaces.
The following alternate syntax can be used when the code in a handler is more complex:
<binding id="binding-name">
<handlers>
<handler event="event-name">
-- handler code goes here --
</handler>
</handlers>
</binding>The following example adds some key handlers to create a very primitive local clipboard:
Example 10.6.1: Source<binding id="clipbox">
<content>
<xul:textbox/>
</content>
<field name="clipboard"/>
<handlers>
<handler event="keypress" key="x" modifiers="control"
action="this.clipboard=document.getAnonymousNodes(this)[0].value; document.getAnonymousNodes(this)[0].value='';"/>
<handler event="keypress" key="c" modifiers="control"
action="this.clipboard=document.getAnonymousNodes(this)[0].value;"/>
<handler event="keypress" key="v" modifiers="control"
action="document.getAnonymousNodes(this)[0].value=this.clipboard ? this.clipboard : '';"/>
</handlers>
</binding>The content is a single textbox. A field clipboard has been added to it to store the clipboard contents. This does mean that the clipboard operations are limited to this single textbox. However, each one will have its own buffer.
Three handlers have been added, one for cut, one for copy and the other for paste. Each has its own keystroke that invokes it. The first handler is the cut operation and is invoked when the Control key is pressed along with the x key. The script within the action attribute is used to cut the text from the textbox and put it into the clipboard field. For simplicity, the entire text is cut and not just the selected text. The code works as follows:
this.clipboard=document.getAnonymousNodes(this)[0].value;The first element of the anonymous content array is retrieved which gives a reference to the textbox element, which happens to be the first (and only) element within the content element. The value property is retrieved which will provide the text within the textbox. This is then assigned to the clipboard field. The result is copying the text in the textbox into this special clipboard.
document.getAnonymousNodes(this)[0].value=''The text of the textbox is then assigned a value of a null string. This effectively clears the text in the textbox.
A copy operation is similar but does not the clear the text afterwards. Pasting is the opposite where the value of the textbox is assigned from the value in the clipboard field. If we were creating a real implementation of these clipboard keyboard shortcuts, we would probably use the real clipboard interface and handle the current selection as well.
In this section, we'll look at how to extend existing XBL definitions.
Sometimes you may want to create an XBL widget that is similar to an existing one. For example, let's say you want to create an XBL button with a popup. One way to create this is to duplicate the existing XBL code for buttons. However, it would be better to simply extend the existing button code.
Any binding can be extended with another. The child binding can add properties, methods and event handlers. The child binding will have all the features it defines in addition to the features from the binding it inherits from (and any that binding inherits from and so on up the tree).
To extend an existing binding, add an extends attribute on to the binding tag. For example, the following binding creates a textbox which adds the text 'http://www' to the beginning of its value when the F4 key is pressed.
Example 10.7.1: Source<binding id="textboxwithhttp"
extends="chrome://global/content/bindings/textbox.xml#textbox">
<handlers>
<handler event="keypress" keycode="VK_F4">
this.value="http://www"+value;
</handler>
</handlers>
</binding>The XBL here extends from the XUL textbox element. The URL given in the extends attribute above is the URL of the binding of the textbox binding. This means that we inherit all of the content and behavior provided by the textbox binding. In addition, we add a handler which responds to the keypress event.
The example above is similar to how the URL autocomplete feature works in Mozilla. A textbox that supports autocomplete is just one with a XBL binding that extends the basic textbox.
The autocomplete textbox adds extra event handling so that when a URL is typed, a menu will pop up with possible completions. You can use it in your own applications too. Just create a textbox with two extra attributes.
<textbox type="autocomplete" searchSessions="history"/>
Set the type to autocomplete to add the autocomplete feature to an existing textbox. Set the searchSessions to indicate what type of data to look up. In this case, the value history is used, which looks up URLs in the history. (You can also use the value addrbook to look up addresses in the address book.)
This section will describe an example XBL element.
Let's construct a full example of an XBL element. This will be a widget that stores a deck of objects, each displayed one at a time. Navigation buttons along the bottom will allow the user to cycle through the objects while a text widget between the buttons will display the current page. You could put anything within the pages, however, this widget might be useful for a set of images. We'll call this a slideshow element.
First, let's determine what elements need to go in the XBL content. Because we want page flipping, a deck element would be the most suitable to hold the page content. The content of the pages will be specified in the XUL file, not in XBL, but we'll need to add it inside the deck. The children tag will need to be used. Along the bottom, we'll need a button to go the previous page, a text widget to display the current page number, and a button to go to the next page.
Example 10.8.1: Source<binding id="slideshow">
<content>
<xul:vbox flex="1">
<xul:deck xbl:inherits="selectedIndex" selectedIndex="0" flex="1">
<children/>
</xul:deck>
<xul:hbox>
<xul:button xbl:inherits="label=previoustext"/>
<xul:label flex="1"/>
<xul:button xbl:inherits="label=nexttext"/>
</xul:hbox>
</xul:vbox>
</content>
</binding>This binding creates the slideshow structure that we want. The flex attribute has been added to a number of elements so that it stretches in the right way. The label attributes on the two buttons inherit their values from the bound element. Here, they inherit from two custom attributes, previoustext and nexttext. This makes it easy to change the labels on the buttons. The children of the element that the XBL is bound to will be placed inside the deck. The selectedIndex is inherited by the deck, so we may set the initial page in the XUL.
The following XUL file produces the result in the image.
<box class="slideshow" previoustext="Previous" nexttext="Next" flex="1"> <button label="Button 1"/> <checkbox label="Checkbox 2"/> <textbox/> </box>
The style sheet used here is:
.slideshow {
-moz-binding: url("slideshow.xml#slideshow");
}
The first button, 'Button 1' has been used as the first page of the deck.
The label widget has not appeared as no
value has been specified for it. We could set a
value, but instead it will calculated later.
Next, a property that holds the current page will be added. When getting this custom property, it will need to retrieve the value of the selectedIndex attribute of the deck, which holds the number of the currently displayed page. Similarly, when setting this property, it will need to change the selectedIndex attribute of the deck. In addition, the text widget will need to be updated to display which page is the current one.
<property name="page"
onget="return parseInt(document.getAnonymousNodes(this)[0].childNodes[0].getAttribute('selectedIndex'));"
onset="return this.setPage(val);"/>The 'page' property gets its value by looking at the first element of the anonymous array. This returns the vertical box, so to get the deck, we need to get the first child node of the box. The anonymous array isn't used as the deck is not anonymous from the box. Finally, the value of the selectedIndex attribute is retrieved. To set the page, a method 'setPage' is called which will be defined later.
An onclick handler will need to be added to the Previous and Next buttons so that the page is changed when the buttons are pressed. Conveniently, we can change the page using the custom 'page' property that was just added:
<xul:button xbl:inherits="label=previoustext"
onclick="parentNode.parentNode.parentNode.page--;"/>
<xul:description flex="1"/>
<xul:button xbl:inherits="label=nexttext"
onclick="parentNode.parentNode.parentNode.page++;"/>Because the 'page' property is only on the outer XUL element, we need to to use the parentNode property to get to it. The first parentNode returns the parent of the button which is the horizontal box, the second its parent, the vertical box, and finally, its parent which is the outer box. The 'page' property is incremented or decremented. This will call the onget script to get the value, increment or decrement the value by one, and then call the onset handler to set the value.
Now let's define the 'setPage' method. It will take one parameter, the page number to set the page to. It will need to make sure the page is not out of range and then modify the deck's selectedIndex attribute and the text widget's label attribute.
<method name="setPage">
<parameter name="newidx"/>
<body>
<![CDATA[
var thedeck=document.getAnonymousNodes(this)[0].childNodes[0];
var totalpages=this.childNodes.length;
if (newidx<0) return 0;
if (newidx>=totalpages) return totalpages;
thedeck.setAttribute("selectedIndex",newidx);
document.getAnonymousNodes(this)[0].childNodes[1].childNodes[1]
.setAttribute("value",(newidx+1)+" of "+totalpages);
return newidx;
]]>
</body>
</method>This function is called 'setPage' and takes one parameter 'newidx'. The body of the method has been enclosed inside '<![CDATA[' and ']]>'. This is the general mechanism in all XML files that can be used to escape all of the text inside it. That way, you don't have to escape every less-than and greater-than sign inside it.
Let's break down the code piece by piece.
var thedeck=document.getAnonymousNodes(this)[0].childNodes[0];Get the first element of the anonymous content array, which will be the vertical box, then get its first child, which will be the deck element.
var totalpages=this.childNodes.length;Get the number of children that the bound box has. This will give the total number of pages that there are.
if (newidx<0) return 0;If the new index is before the first page, don't change the page and return 0. The page should not change to a value earlier than the first page.
if (newidx>=totalpages) return totalpages;If the new index is after the last page, don't change the page and return the last page's index. The page should not change to one after the last page.
thedeck.setAttribute("selectedIndex",newidx);
Change the selectedIndex attribute on the deck.
This causes the requested page to be displayed.
document.getAnonymousNodes(this)[0].childNodes[1].childNodes[1].setAttribute("value",(newidx+1)+" of "+totalpages);
This line modifies the label element so that it displays the current page
index. The label element can be retrieved by getting the first element of
anonymous content (the vertical box), the second child of that label element
(the horizontal box), and then the second element of that box. The
value attribute is changed to read '1 of 3' or
something similar. Note that one is added to the index because indicies
start at 0.
We will also need a constructor to initialize the label element so that it displays correctly when the slideshow is first displayed. We use similar code as to the method above to set the page number. The reference to 'this.page' will call the onget script of the page property, which in turn will retrieve the initial page from the selectedIndex attribute.
<constructor>
var totalpages=this.childNodes.length;
document.getAnonymousNodes(this)[0].childNodes[1].childNodes[1]
.setAttribute("value",(this.page+1)+" of "+totalpages);
</constructor>We can add some additional features as well. Some keyboard shortcuts could be used for the Previous and Next buttons, (say backspace and the Enter key). First and Last buttons could be added to go to the first and last pages. The label element could be changed to a field where the user could enter the page to go to, or a popup could be added to allow selection of the page from a menu. We could also add a border around the deck with CSS to make it look a bit nicer.
The final code is as follows:
Example 10.8.2: Source<binding id="slideshow">
<content>
<xul:vbox flex="1">
<xul:deck xbl:inherits="selectedIndex" selectedIndex="0" flex="1">
<children/>
</xul:deck>
<xul:hbox>
<xul:button xbl:inherits="label=previoustext"
onclick="parentNode.parentNode.parentNode.page--;"/>
<xul:description flex="1"/>
<xul:button xbl:inherits="label=nexttext"
onclick="parentNode.parentNode.parentNode.page++;"/>
</xul:hbox>
</xul:vbox>
</content>
<implementation>
<constructor>
var totalpages=this.childNodes.length;
document.getAnonymousNodes(this)[0].childNodes[1].childNodes[1]
.setAttribute("value",(this.page+1)+" of "+totalpages);
</constructor>
<property name="page"
onget="return parseInt(document.getAnonymousNodes(this)[0].childNodes[0].getAttribute('selectedIndex'));"
onset="return this.setPage(val);"/>
<method name="setPage">
<parameter name="newidx"/>
<body>
<![CDATA[
var thedeck=document.getAnonymousNodes(this)[0].childNodes[0];
var totalpages=this.childNodes.length;
if (newidx<0) return 0;
if (newidx>=totalpages) return totalpages;
thedeck.setAttribute("selectedIndex",newidx);
document.getAnonymousNodes(this)[0].childNodes[1].childNodes[1]
.setAttribute("value",(newidx+1)+" of "+totalpages);
return newidx;
]]>
</body>
</method>
</implementation>
</binding>We've already seen some features of windows. We'll look at some more in this section.
You can create a second window for your application in the same manner as you would create the first one. Just create a second XUL file with the window code in it. As in HTML, you can use the window.open function to open the second window. This function will return a reference to the newly opened window. You can use this reference to call functions of the other window.
The open function takes three arguments. The first is the URL of the file you wish to open. The second is an internal name of the window. The third is a list of display flags. The flag 'chrome' is important to open the window as a chrome file. If you do not add the 'chrome' flag, the file will open up as the content in a browser window.
For example:
window.open("chrome://findfile/content/findfile.xul","findfile","chrome");You should have noticed that whenever elements were added to a window, the window's width expanded to fit the new elements. The window is really just a box which is flexible and defaults to vertical orientation. You can also specify the width and height directly on the window tag. This, of course, causes the window to be displayed in a specific size. If you leave it out, the size is determined by the elements that are in it.
<window id="findfile-window" title="Find Files" width="400" height="450" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
In this example, the window will open with a width of 400 pixels and a height of 450 pixels. Even if there aren't enough elements to fit this size, the window will still open at this size and there will be blank space in the remaining area. If there are too many elements, the window will not be large enough to fit the elements. The user will have to resize the dialog. You have to be careful when specifying a width and height that the window is not too small or too big.
Note that you must specify both the width and the height. If you only specify one, the other will be set to 0. To have the window set its size automatically, leave both the width and height out.
The width and height only specify the initial size of the window. The user may still resize the window to another size, assuming that the window is resizable.
The flags below can be passed as part of the third argument to the window.open function. Your operating system may not support all of them. You can also use any of the pre-existing flags, which you should find in a JavaScript reference. You may disable a feature by setting it to 'no', for example 'dialog=no'.
Another feature that is useful only during development is to enable the debugging mode of a window. To do this, add a debug attribute to the window and set it to true. This will cause the window to display the boxes and spacer so you can see what is happening. The example below shows how to use it.
<window id="findfile-window" title="Find Files" debug="true" xmlns:html="http://www.w3.org/1999/xhtml" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
The image below shows the effect when applied to a simple window.
In the image, you might notice a number of additional boxes in place. This is because each XUL element is made up of a number of boxes itself, defined with XBL. You can normally ignore these. You can specify the debug attribute on any box, not just on a window.
A XUL application will often require dialogs to be displayed. This section describes how one might construct them.
The open function is used to open a window. A related function is openDialog. This function has several main differences. It will display a dialog instead of a window which implies that it is asking something of the user. It may have subtle differences in the way it works and appears to the user. These differences will vary on each platform.
In addition, the openDialog function can take additional arguments beyond the first three. These arguments are passed to the new dialog and placed in an array stored in the new window's arguments property. You can pass as many arguments as necessary. This is a convenient way to supply default values to the fields in the dialog.
var somefile=document.getElementById('enterfile').value;
window.openDialog("chrome://findfile/content/showdetails.xul","showmore",
"chrome",somefile);In this example the dialog 'showdetails.xul' will be displayed. It will be passed one argument, 'somefile', which was taken from the value of an element with the id enterfile. In a script used by the dialog, we can then refer to the argument using the window's arguments property. For example:
var fl=window.arguments[0];
document.getElementById('thefile').value=fl;This is an effective way to pass values to the new window. You can pass values back from the opened window to the original window in one of two ways. First, you could use the window.opener property which holds the window that opened the dialog. Second, you could pass a function or object as one of the arguments, and then call the function or modify the object in the opened dialog.
The dialog element should be used in place of the window element when creating a dialog. It provides the useful capability to construct up to four buttons along the bottom of the dialog for OK, Cancel and so on. You do not need to include the XUL for each button but you do need to supply code to handle when the user presses each button. This mechanism is necessary because different platforms have a specific order in which the buttons appear.
Example 11.2.1: Source View<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
<dialog id="donothing" title="Do Nothing"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
buttons="accept,cancel"
ondialogaccept="return doOK();"
ondialogcancel="return doCancel();">
<script>
function doOK()
{
alert("You pressed OK!");
return true;
}
function doCancel()
{
alert("You pressed Cancel!");
return true;
}
</script>
<description value="Select a button"/>
</dialog>You may place any elements that you wish in a dialog. The dialog element has some additional attributes that windows do not have. The buttons attribute is used to specify which buttons should appear in the dialog. The following values may be used, seperated by commas:
You can set code to execute when the buttons are pressed using the ondialogaccept, ondialogcancel, ondialoghelp and ondialogdisclosure attributes. If you try the example above, you will find that the doOK function is called when the OK button is pressed and the doCancel function is called when the Cancel button is pressed.
The two functions doOK and doCancel return true which indicate that the dialog should be closed. If false was returned, the dialog would stay open. This would be used if an invalid value was entered in a field in the dialog.
A common type of dialog in one where the user can select a file to open or save.
A file picker is a dialog that allows the user to select a file. It is most commonly used for the Open and Save As menu commands, but you can use it any place in which the user needs to select a file. The XPCOM interface nsIFilePicker is used to implement a file picker.
You can use the file picker in one of three modes:
The appearance of the dialog will be different for each type and will vary on each platform. Once the user selects a file or folder, it can be read from or written to.
The file picker interface nsIFilePicker is responsible for displaying a dialog in one the three modes. You can set a number a features of the dialog by using the interface. When the dialog is closed, you can use the interface functions to get the file that was selected.
To begin, you need to create a file picker component and initialize it.
var nsIFilePicker = Components.interfaces.nsIFilePicker;
var fp = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(nsIFilePicker);
fp.init(window, "Select a File", nsIFilePicker.modeOpen);First, a new file picker object is created and stored in the variable 'fp'. The 'init' function is used to initialize the file picker. This function takes three arguments, the window that is opening the dialog, the title of the dialog and the mode. The mode here is 'modeOpen' which is used for an Open dialog. You can also use 'modeGetFolder' and 'modeSave' for the other two modes. These modes are constants of the nsIFilePicker interface.
There are two features you can set of the dialog before it is displayed. The first is the default directory that is displayed when the dialog is opened. The second is a filter that indicates the list of file types that are displayed in the dialog. This would be used, for example, to hide all but HTML files.
You can set the default directory by setting the displayDirectory property of the file picker object to a directory. The directory should be an nsILocalFile object. If you do not set this, a suitable default will be selected for you. To add filters, call the appendFilters function to set the file types that you wish to have displayed.
fp.appendFilters(nsIFilePicker.filterHTML | nsIFilePicker.filterImages); fp.appendFilters(nsIFilePicker.filterText | nsIFilePicker.filterAll);
The first example will add filters for HTML and for image files. The user will only be able to select those types of files. The manner in which this is done is platform specific. On some platforms, each filter will be separate and the user can choose between HTML files and image files. The second example will add filters for text files and for all files. The user therefore has the option to display text files only or all files.
You can also use 'filterXML' and 'filterXUL' to filter for XML and XUL files. If you would like to filter for custom files, you can use the appendFilter function to do this:
fp.appendFilter("Audio Files","*.wav; *.mp3");This line will add a filter for Wave and MP3 audio files. The first argument is the title of the file type and the second is a semicolon-seperated list of file masks. You can add more masks or fewer masks as needed. You can call appendFilter as many times as necessary to add additional filters. The order you add them determines their priority. Typically, the first one added is selected by default.
Finally, you can show the dialog by calling the show function. It takes no arguments but returns a status code that indicates what the user selected. Note that the function does not return until the user has selected a file. The function returns one of three constants:
You should check the return value and then get the file object from the file picker using the file property.
var res=fp.show();
if (res==nsIFilePicker.returnOK){
var thefile=fp.file;
// --- do something with the file here ---
}Many applications use wizards to help the user through complex tasks. XUL provides a way to create wizards easily.
A wizard is a special type of dialog that contains a number of pages. Navigation buttons appear on the bottom of the dialog to switch between pages. The wizards are usually used to help the user perform a complex task. Each page contains a single question or a set of related questions. After the last page, the operation is carried out.
XUL provides a wizard element which can be used to create wizards. The contents inside the wizard element include all the content of every page of the wizard. Attributes placed on the wizard are used to control the wizard navigation. When creating a wizard, use the wizard tag instead of the window tag.
Note that wizards currently only work properly from chrome URLs.
The wizard consists of several sections, although the exact layout will vary for each platform. The wizard will generally be displayed like those on the user's platform. A typical layout will include a title across the top, a set of navigation buttons across the bottom and the page contents in between.
The title across the top is created using the title attribute, much like one would do for regular windows. The navigation buttons are created automatically. The pages of the wizard are created using the wizardpage element. You can place whatever content you want inside each wizardpage. Here is an example wizard:
Example 11.4.1: Source
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<wizard id="example-window" title="Select a Dog Wizard"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<wizardpage>
<description>
This wizard will help you select the type of dog that is best for you."
</description>
<label value="Why do you want a dog?"/>
<menulist>
<menupopup>
<menuitem label="To scare people away"/>
<menuitem label="To get rid of a cat"/>
<menuitem label="I need a best friend"/>
</menupopup>
</menulist>
</wizardpage>
<wizardpage description="Dog Details">
<label value="Provide additional details about the dog you would like:"/>
<radiogroup>
<caption label="Size"/>
<radio value="small" label="Small"/>
<radio value="large" label="Large"/>
</radiogroup>
<radiogroup>
<caption label="Gender"/>
<radio value="male" label="Male"/>
<radio value="female" label="Female"/>
</radiogroup>
</wizardpage>
</wizard>
This wizard has two pages, one that has a drop-dowm menu and the other with a set of radio buttons. The wizard will be formatted automatically, with a title across the top and a set of buttons along the bottom. The user can navigate between the pages of the wizard with the Back and Next buttons. These buttons will enable and disable themselves at the appropriate moments. In addition, on the last page, the Finish button will appear. All of this is automatic, so you don't have to do anything to manipulate the pages.
The description attribute may optionally placed on a wizardpage element to provide a sub-caption for that page. In the example above, it has been placed on the second page, but not the first page.
You will generally want to do something once the Finish button is pressed. You can set an attribute onwizardfinish on the wizard element to accomplish this. Set it to a script which performs whatever task you want and then returns true. This script might be used to save the information that the user entered during the wizard.
For example:
<wizard id="example-window" title="Select a Dog Wizard" onwizardfinish="return saveDogInfo();" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
When the user clicks the Finish button, the function 'saveDogInfo' will be called, which would be defined in a script file to save the information that was entered. If the function returns true, the wizard closes. If it returns false, then the wizard does not close, which might occur if the function 'saveDogInfo' encountered invalid input, for example.
There are also related onwizardback, onwizardnext and onwizardcancel attributes, which are called when the Back, Next and Cancel buttons are pressed. These functions are called regardless of which page is currently displayed.
To have different code called depending on which page you are on, use the onpagerewound or onpageadvanced attributes on a wizardpage element. They work similar to the other functions except that you can use different code for each page. This allows you to validate the input entered on each page before the user continues.
A third method is to use the onpagehide and onpageshow attributes on the wizardpage element. They will be called when the page is hidden or shown, regardless of which button was pressed (except when Cancel is pressed -- you need to use onwizardcancel to check for this.)
These three methods should provide enough flexibility to handle navigation as you need to. The following is a summary of attribute functions that are called when the user presses Next, in the order that they will be checked. As soon as one returns false, the navigation will be cancelled.
| Attribute | Place on Tag | When it is Called |
| pagehide | wizardpage | Called on the page that the user is leaving. |
| pageadvanced | wizardpage | Called on the page the user is leaving. |
| wizardnext | wizard | Called on the wizard. |
| pageshow | wizardpage | Called on the page that the user is entering. |
A similar process occurs for the Back button.
This section describes some additional features of wizards.
Normally, a wizard displays each wizardpage in the order that you place them in the XUL file. In some cases however, you may want to have different pages of the wizard appear depending on what the user selects in earlier pages.
In this case, place a pageid attribute on each of the pages. This should be set to an indentifer for each page. Then, to navigate to a page, use one of two methods:
For example, here are a set of wizard pages (the inner content has been omitted):
<wizardpage pageid="type" next="font"> <wizardpage pageid="font" next="done"> <wizardpage pageid="color" next="done"> <wizardpage pageid="done">
The wizard always starts at the first page, which in this case has the page ID type. The next page is the one with the page ID font, so the wizard will navigate to that page next. On the page with the page ID font, we can see that the next page is done, so that page will be displayed afterwards. The page with the page ID done has no next attribute, so this will be the last page. A script will adjust the next attributes as necessary to go to the page with the page ID color when needed.
The wizard works much like a tabbed panel, except that the tabs are not displayed and the user navigates between pages by using the buttons along the bottom. Because all of the pages are part of the same file, all of the values of the fields on all pages will be remembered. Thus, you do not have to load and save information between pages.
However, you may want to do some validation of each field on each page. For this, use the handlers described in the previous section. If a field is invalid, you might display an alert. In some cases, it would be more convenient to disable the Next button until valid input has been entered.
The wizard has a property canAdvance, which can be set to true to indicate that the Next button should be enabled. If set to false, the Next button is disabled. You can change the property when invalid or valid data has been entered.
In the following example, the user must enter a secret code into a textbox on the first page of the wizard. The function checkCode is called whenever the first page is shown as indicated by the onpageshow attribute. It is also called whenever a key is pressed in the textbox, to determine whether the Next button should be enabled again.
Example 11.5.1: Source
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<wizard id="theWizard" title="Secret Code Wizard"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script>
function checkCode()
{
document.getElementById('theWizard').canAdvance=
(document.getElementById('secretCode').value == "cabbage");
}
</script>
<wizardpage onpageshow="checkCode();">
<label value="Enter the secret code:"/>
<textbox id="secretCode" onkeyup="checkCode();"/>
</wizardpage>
<wizardpage>
<label value="That is the correct secret code."/>
</wizardpage>
</wizard>There is also a corresponding canRewind property that you can use to enable or disable the Back button. Both properties are adjusted automatically as you switch pages. Thus, the Back button will be disabled on the first page so you don't have to set it yourself.
Another useful property of the wizard is currentPage, which holds a reference to the currently displayed wizardpage. You can also modify the current page by changing this property. If you do change it, the various page change events will still be fired.
This section will describe overlays which can be used to separate common content.
In a simple application with only one window, you will generally have only one XUL file, along with a script file, a style sheet, a DTD file and perhaps some images. Some applications will have a number of dialogs associated with them also. These will be stored in separate XUL files. More sophisticated applications will contain many windows and dialogs.
An application that has several windows will have numerous elements or parts of the user interface that are common between each window. For example, each of Mozilla's components share some common elements. Some of the menus are similar, such as the Tools and Help menus, the sidebar is similar, and each window shares some common global keyboard shortcuts.
One could handle this by re-implementing the similar elements and functions in each file that are needed. However, this would be difficult to maintain. If you decide to change something, you would have to change it in numerous places. Instead, it would be better to use a mechanism that allows you to separate the common elements and have them shared between windows. You can do this with overlays.
Within an overlay, you may place elements that are shared between all windows that use that overlay. Those elements are added into the window at locations determined by their ids.
For example, let's say you want to create a help menu that is shared between several windows. The help menu will be placed in an overlay, using the same XUL that you would use normally. The menu will be given an id attribute to identify it. Each window will import the overlay using a directive which will be described in a moment. To use the help menu as defined in the overlay, you only need to add a single menu element with the same value for its id attribute as used in the overlay. This menu does not need to contain any children as those will be placed in the overlay.
When a window with an overlay is opened, the elements in both the window and the overlay with the same ids are combined together. The children of matching elements are added to the end of the set of children in the window's element. Attributes that are present on the overlay's elements will be applied to the window's elements. These details will be explained in more detail later.
To import an overlay into a window, use the syntax described below. Let's add this near the top of the findfiles dialog XUL file.
<?xul-overlay href="chrome://findfile/content/helpoverlay.xul"?>This line should be added somewhere near the top of the file, usually just before any DTDs are declared. In the example above, the window is importing an overlay stored in the file helpoverlay.xul.
The overlay itself is a XUL file that contains an overlay element instead of a window element. Other than that, it is much the same. You can import overlays from inside other overlays. Overlays can also have their own stylesheets, DTDs and scripts. The example below shows a simple Help menu stored in an overlay.
Example 11.6.1: Source<?xml version="1.0"?>
<!DOCTYPE overlay SYSTEM "chrome://findfile/locale/findfile.dtd">
<overlay id="toverlay"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<menu id="help-menu">
<menupopup id="help-popup">
<menuitem id="help-contents" label="&contentsCmd.label;"
accesskey="&contentsCmd.accesskey;"/>
<menuitem id="help-index" label="&indexCmd.label;"
accesskey="&indexCmd.accesskey;"/>
<menuitem id="help-about" label="&aboutCmd.label;"
accesskey="&aboutCmd.accesskey;"/>
</menupopup>
</menu>
</overlay>The overlay element surrounds the overlay content. It uses the same namespace as XUL window files. Defined within the overlay is a single menu with three items in it. The id of this menu is help-menu. This means that its content will be added to the window where a similar element exists with the same id value. If such an element does not exist, that part of the overlay is ignored. The overlay can contain as many elements as necessary. Note that the overlay needs to include the DTD file also. We use the same one as the main window here, but normally you would create a separate DTD file for each overlay.
Next, we need to add the help menu to the findfiles dialog window. To do this just add a menu with the same id in the right location. The most likely place is just after the edit menu.
<menu id="edit-menu" label="Edit" accesskey="e">
<menupopup id="edit-popup">
<menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;"
key="cut_cmd"/>
<menuitem label="©Cmd.label;" accesskey="©Cmd.accesskey;"
key="copy_cmd"/>
<menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;"
key="paste_cmd" disabled="true"/>
</menupopup>
</menu>
<menu id="help-menu" label="&helpCmd.label;"
accesskey="&helpCmd.accesskey;"/>
</menubar>Here, the help menu element contains no content. The items from the menu are taken from the overlay because the ids match. We can then import the overlay in other windows and only have the contents of the help menu defined in one place. We need to add some lines to the DTD file as well:
<!ENTITY helpCmd.label "Help"> <!ENTITY helpCmd.accesskey "h"> <!ENTITY contentsCmd.label "Contents"> <!ENTITY indexCmd.label "Index"> <!ENTITY aboutCmd.label "About..."> <!ENTITY contentsCmd.accesskey "c"> <!ENTITY indexCmd.accesskey "i"> <!ENTITY aboutCmd.accesskey "a"> <!ENTITY findfilehelpCmd.label "Find files help"> <!ENTITY findfilehelpCmd.accesskey "f">
We will use the last two entities in a moment.
We can further reduce the amount of code within the window by putting the attributes on the help menu (label and accesskey in this example) in the overlay instead. Those attributes will be inherited by the element. If both the element and the window specify the same attribute, the value in the overlay will override the element's value.
Let's change the help menu in this manner.
findfile.xul: <menu id="help-menu"/> helpoverlay.xul: <menu id="help-menu" label="&helpCmd.label;" accesskey="&helpCmd.accesskey;">
If there is content inside both the XUL window and in the overlay, the window's content will be used as is and the overlay's content will be appended to the end. This following example demonstrates this:
stopandgo.xul:
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
<window title="Stop and Go" id="test-window"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<?xul-overlay href="chrome://findfile/content/toverlay.xul"?>
<box id="singlebox">
<button id="gobutton" label="Go"/>
<button id="stopbutton" label="Stop"/>
</box>
</window>
toverlay.xul:
<?xml version="1.0"?>
<overlay id="toverlay"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<box id="singlebox">
<button id="backbutton" label="Back"/>
<button id="forwardbutton" label="Forward"/>
</box>
</overlay>
In this example, the box with the identifier singlebox contains its own content. The elements are combined and the two buttons from the overlay are added to the end of the box.
We can use this technique in the find files dialog also:
findfile.xul:
<menu id="help-menu">
<menupopup id="help-popup">
<menuitem id="help-findfiles" label="&findfilehelpCmd.label;" accesskey="&findfilehelpCmd.accesskey;"/>
</menupopup>
</menu>
</menubar>The id attribute of the menupopup element also matches the one in the overlay. This will cause the items to be merged into the same popup. Overlays will merge items with the same ids even if they are inside of other elements.
However, we may have wanted to have the menu items from the overlay in the previous example placed at the beginning of the menu instead of at the end. XUL provides a mechanism that allows you to not only place them at the beginning but to put some of the items at the top and others at the bottom (or anywhere in-between). This allows you to overlay menus, toolbars and other widgets at the exact location that you wish.
To do this, use the insertbefore attribute on the menuitems. Its value should be the id of an element that you want to insert the item before. Alternatively, you can use the insertafter attribute to indicate which element to insert after. These attributes only affect the element the attributes are added to. If one element is 'inserted before', the remaining elements are still added to the end. If you want to have all the elements appear before, you must put the insertbefore attribute on all elements.
In addition, you can use the position attribute if you want to specify a specific index position. The first position is 1.
Let's say that we wanted the Contents and Index items from the previous example to appear before the Find files help item and the About item to appear after. To do this we add the insertbefore attribute to both the Contents and Index menu items. For completeness, you could add an insertafter attribute on the About menu too, but it isn't necessary because it appears at the end by default.
In the help menu example above, the id of the menu item is help-findfiles. Thus, we need to set the insertbefore attributes to this id. The example below shows the changes:
<menupopup id="help-popup"> <menuitem id="help-contents" label="Contents" insertbefore="help-findfiles"/> <menuitem id="help-index" label="Index" insertbefore="help-findfiles"/> <menuitem id="help-about" label="About..."/> </menupopup>
Now, when a window using the help overlay (such as the find files dialog) is opened, the following will occur:
Actually, the values of insertbefore and insertafter can be comma-separated lists, in which case the first id in the list that is found in the window is used to determine the position.
This section describes how to apply overlays to files that don't import them.
Overlays have another very useful feature. In the examples in the previous section, the overlays were imported by the window. You can also go the other way and have the overlays specify which windows that they apply to. You specify this by modifying the contents.rdf file for your package. This is useful because the overlay can modify the user interface of another package without changing the other package. For example, you could add menu items or toolbars to the Mozilla browser window.
We'll use this feature to add a toolbar to the Mozilla browser window. The Mozilla Mail application uses overlays to add content to the browser window. For example, if Mail is not installed, there will be no New Message command. However, if Mail is installed, an overlay will be applied to the menu to add the New Message command. Below, we'll add a find files toolbar to the browser. It probably wouldn't be useful to have this feature, but we'll do it anyway.
Mozilla allows you to add a list of overlays to the contents.rdf file that you use to list chrome packages, skins and locales. Once you have created an overlay, you can add it to the contents.rdf file. Then add items, one for each window that you want the overlay to apply to.
First, let's create a simple overlay. It will just have a few fields for entering a filename and directory to search. Call the file foverlay.xul and add it to the findfile directory along with findfile.xul.
Example 11.7.1: Source<?xml version="1.0"?>
<overlay
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<toolbox id="navigator-toolbox">
<toolbar id="findfile_toolbar">
<label control="findfile_filename" value="Search for files named:"/>
<textbox id="findfile_filename"/>
<label control="findfile_dir" value="Directory:"/>
<textbox id="findfile_dir"/>
<button label="Browse..."/>
</toolbar>
</toolbox>
</overlay>You can view this by changing the overlay to a window. The only thing that is special here is the id used on the toolbox. This value (navigator-toolbox) is the same as the identifier of the toolbox in the browser window (navigator.xul). This means that the overlay will apply to the toolbox in the browser window and the content will be added as an extra toolbar.
To add this overlay to the manifest file, we need to add two resources. First, we add one for each window that we are applying an overlay to. The code below should be added into content.rdf just before the closing RDF tag.
<RDF:Seq about="urn:mozilla:overlays"> <RDF:li resource="chrome://navigator/content/navigator.xul"/> </RDF:Seq>
This declares that we are adding a overlay window, a child of the root overlay node (urn:mozilla:overlays). You can add additional nodes for any other windows that you want to apply overlays to by adding additional li nodes.
Next, we add a node for each overlay to apply to the window. In this case, we only have one, but we could apply others also. Add these lines just after the previous lines.
<RDF:Seq about="chrome://navigator/content/navigator.xul"> <RDF:li>chrome://findfile/content/foverlay.xul</RDF:li> </RDF:Seq>
Mozilla reads this information and builds a list of overlays that are applied to other windows. It stores this information in the chrome/overlayinfo directory. You do not need to manually modify the files in this directory. It is automatically generated and modified when Mozilla is first run or when new packages are installed. However, you can force the data to be rebuilt by deleting this directory and the chrome.rdf file.
As a side note, you can use a similar technique to apply extra style sheets. The following example shows how:
<RDF:Seq about="urn:mozilla:stylesheets"> <RDF:li resource="chrome://messenger/content/messenger.xul"/> </RDF:Seq> <RDF:Seq about="chrome://messenger/content/messenger.xul"> <RDF:li>chrome://blueswayedshoes/skin/myskinfile.css</RDF:li> </RDF:Seq>
This section will describe packaging a XUL application into an installer.
Mozilla provides a mechanism which can be used to package XUL windows, scripts, skins and other files into single file installers. You can place this installer file somewhere for users to download. A simple script can be used to have the package downloaded and installed. This mechanism is called XPInstall (Cross platform Install).
XPInstall installers are packaged into JAR files. Inside the JAR file, you can add all the various files that you want to have installed. In addition, installers should contain an install script (a file named install.js) which can be used to script the installation process. This script has access to various install functions which can be used to install files and components.
The JAR file installers typically have the extension .xpi (pronounced zippy) to distinguish them from other archives. The installers will be usually used to install Mozilla components such as new skins, plugins and new packages.
There are several steps involved in launching an installer and installing components. These are described step by step below.
As indicated above, the install process is started by an install trigger. This involves the use of the special global object InstallTrigger. It contains a number of methods which can be used to start an installation. You can use this object in local or remote content, meaning that it is suitable for a download from a Web site.
Let's create an example install trigger. This involves the use of the function InstallTrigger.install. This function takes two arguments, the first is a list of packages to install, and the second is a callback function which will be called when the installation is complete. Here is an example:
function doneFn ( name , result ){
alert("The package " + name + " was installed with a result of " + result);
}
var xpi = new Object();
xpi["Calendar"] = "calendar.xpi";
InstallTrigger.install(xpi,doneFn);First, we define a callback function doneFn which will be called when the install is complete. You can name the function whatever you like of course. This function has two arguments. The first is the name of the package that was just installed. This is important if you are installing multiple components. The second argument is a result code. If the result is 0, the installation completed successfully. If the result is non-zero, an error occured and the value is an error code. The function doneFn here just displays an alert box to the user.
Next, we create an array xpi which will hold the name (Calendar) and URL (calendar.xpi) of the installer. You can add an additional similar such line for each package you wish to have installed. Finally, we call the install function.
When this section of script is executed, the file calendar.xpi will be installed.
Let's try this with the find files dialog.
function doneFn ( name , result ){
if (result) alert("An error occured: " + result);
}
var xpi = new Object();
xpi["Find Files"] = "findfile.xpi";
InstallTrigger.install(xpi,doneFn);The installer XPI file is required to contain one file called install.js which is a JavaScript file which is executed during the installation. The remaining files are the files to be installed. These files will typically be placed inside a directory in the archive but they do not have to be. For chrome files, they might be structured like the chrome directory.
Often, the only files placed in an XPI archive will be the install script (install.js) and a JAR file. This JAR file contains all of the files used by your application. The components provided with Mozilla are stored in this manner.
Because the XPI file is just a special ZIP file, you can create it and add files to it using a zip utility.
For the find files dialog, we'll create a structure in the archive much like the following:
install.js
findfile
content
contents.rdf
findfile.xul
findfile.js
skin
contents.rdf
findfile.css
locale
contents.rdf
findfile.dtd
A directory has been added for each part of the package, the content, the skin and the locale. The contents.rdf files have also been added because they will be needed to register the chrome files.
This section describes the install script.
You will usually want some form of control over your install process. For example, you may wish to check versions of files and only install updated files, or perhaps you wish to apply patches to existing files. The install script is even flexible enough to allow you to uninstall files. For this reason, installers include an install script to handle the installation process.
The installer script must be called install.js and must be placed at the top level of the installer archive. The script will contain JavaScript code which calls a number of install functions.
In an HTML document, or a XUL document, the window object is the root global object. That means that you can call the methods of the window object with the qualifier before it, which means that window.open(...) can simply be written open(...). In an install script, there is no associated window, however the global object will be an Install object which contains a number of functions to customize the install process. Some of the Install object's functions will be described below.
The install script should take the following steps:
It is important to note that during step two, you only indicate which files should be installed and any other operations you wish to have happen. No files get copied until step three. Because of this, you can easily specify a number of files to be installed, come across some kind of error, and abort the whole process without modifying the user's system.
Mozilla maintains a file which is a registry of all the components that are currently installed. Components include new chrome packages, skins and plugins. When a new component is installed, the registry gets updated. The registry also stores the set of files and version information about the installed components. That way, it is easier to check if a version of your component is already present and only update it if necessary.
The component registry works somewhat like the Windows registry does. It consists of a hierarchy of keys and values. You don't need to know much about it to create XUL applications unless you are creating your own XPCOM components.
What you do need to know for an installation is that the registry stores a set of information about your application, such as the file list and versions. All of this information is stored in a key (and within subkeys) that you provide in the installation script (in step 1 mentioned above).
This key is structured as directory-like path of the following form:
/Author/Package Name
Replace the word Author with your name and replace the Package Name with the name of the package that you are installing. For example:
/Xulplanet/Find Files /Netscape/Personal Security Manager
The first example is what we'll use for the find files dialog. The second is the key used for the Personal Security Manager.
The Install object has a function, initInstall which can be used to initialize for the installation. It should be called at the beginning of your installation script. The syntax of this function is as follows:
initInstall( packageName , regPackage , version );
Example:
initInstall("Find Files","/Xulplanet/Find Files","0.5.0.0");The first argument is the name of the package in user-readable form. The second argument is the registry key used to hold the package information as described earlier. The third argument is the version of the package being installed.
Next, we need to set the directory where the files will be installed. There are two ways to do this. The simple method assigns an install directory and installs all files into it. The second method allows you to assign a destination on a per-file (or directory) basis. The first method is described below.
The function setPackageFolder assigns an installation directory. For the find files dialog, we will install the files into the chrome directory. (We could actually put them anywhere though.) The setPackageFolder takes one argument, the directory to install to. For maximum portability, you can't specify a string name for the directory. Instead, you specify an identifier of a known directory and get subdirectories of it. Thus, if your application needed to install some system libraries, you don't need to know the name of those directories.
The directory identifiers are listed in the reference. For the chrome directory, the directory identifier is 'Chrome'. The getFolder function can be used to get one of these special directories. This function takes two arguments, the first is the identifier and the second is a subdirectory. For example:
findDir = getFolder("Chrome","findfile");
setPackageFolder(findDir);Here, we get findfile folder in the Chrome folder and pass it directly to the setPackageFolder function. The second argument to getFolder is the subdirectory which we are going to install into, which doesn't have to exist. You can leave this argument out entirely if you don't need one.
Next, you need to specify which files should be installed. This involves the use of two functions, addDirectory and addFile. The addDirectory function tells the installer that a directory from the XPI archive (and all of its contents) should be installed to a particular location. The addFile is similar but for a single file.
Both the addDirectory and addFile functions have various forms. The simplest takes only one argument, the directory from the installer to install to the assigned installation directory.
addDirectory ( dir );
addFile ( dir );
Example:
addDirectory("findfile");The example above will specify that the findfile directory from the installer archive should be installed. We can call these functions multiple times to install other files.
Next, we'll want to register the find files in the chrome system so that it can be used with a chrome URL. This can be done with the registerChrome function. It takes two arguments, the first is the type of chrome to register (content, skin or locale). The second is the directory containing the contents.rdf file to register. Because the find files dialog contains content, a skin file and a locale file, registerChrome will need to be called three times.
registerChrome(Install.CONTENT | Install.DELAYED_CHROME, getFolder(findDir, "content")); registerChrome(Install.SKIN | Install.DELAYED_CHROME, getFolder(findDir, "skin")); registerChrome(Install.LOCALE | Install.DELAYED_CHROME, getFolder(findDir, "locale"));
The DELAYED_CHROME flag is used to indicate that the chrome should be installed the next time Mozilla is run.
The addDirectory and addFile functions don't copy any files. They only state which files should be installed. Similarly, registerChrome only states that chrome should be registered. To complete the process and begin copying files, call the performInstall function. It takes no arguments.
The final script for installing the find files component is shown below:
initInstall("Find Files","/Xulplanet/Find Files","0.5.0.0");
findDir = getFolder("Chrome","findfile");
setPackageFolder(findDir);
addDirectory("findfile");
registerChrome(Install.CONTENT | Install.DELAYED_CHROME, getFolder(findDir, "content"));
registerChrome(Install.SKIN | Install.DELAYED_CHROME, getFolder(findDir, "skin"));
registerChrome(Install.LOCALE | Install.DELAYED_CHROME, getFolder(findDir, "locale"));
performInstall();This section describes some more specifics of installers.
The previous section described a basic installer. You may wish to perform some more elaborate processing during the installation. For example, you may want to install a package only when certain conditions are met, such as having a particular library installed.
In addition to the Install object, a File object is also available during an installation script. It provides some functions which can be used to examine and modify files on disk. You can use these to move, copy or delete files before or after the files are installed. For example, you might want to make a backup of some files first.
The following code will make a copy of the file "/bin/grep" and put it in the directory "/main".
var binFolder=getFolder("file:///","bin");
var grep=getFolder(binFolder,"grep");
var mainFolder=getFolder("file:///","main");
File.copy(grep,mainFolder);The first line will retrieve a reference to the /bin directory. The text 'file:///' is a special string which means the root of the filesystem. From there, we get the file 'grep' which is contained inside the 'bin' directory. If this file does not exist, an error will occur during the installation. Next, we get the 'main' folder, again from the file system root. Finally, we call the File.copy function which copies the source file to the destination.
Functions also exist to move, rename and execute files. Thus, you can move files that might conflict with your package out of the way.
You will likely want to handle errors gracefully. This will occur if a file or directory cannot be found, there is insufficient disk space or for a number of other reasons.
You can use the getLastError function to determine whether an error occured. If it returns 0, no error occured. Otherwise, the number will be an error code which indicates the type of error that occured. You can call this function at any point during the installation script to determine whether an error occured during the last operation.
If an error occurs, you will likely want to abort the installation. You may also want to display an error message to the user. For example, you might put the following as the last section of your script:
if (getLastError()){
performInstall();
}
else {
cancelInstall();
}During installation, a log file is created that contains the operations that are performed. It will also show any errors that occured. The log file can be found in the file 'install.log' in the Mozilla installation directory. A block of text will be added to this file for each installation that occurs.
The logComment function can be used to write a string of text to the log file. It takes one argument, the text to write.